diff --git a/ginger.cabal b/ginger.cabal
--- a/ginger.cabal
+++ b/ginger.cabal
@@ -2,7 +2,7 @@
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 name:                ginger
-version:             0.2.6.0
+version:             0.2.7.0
 synopsis:            An implementation of the Jinja2 template language in Haskell
 description:         Ginger is Jinja, minus the most blatant pythonisms. Wants
                      to be feature complete, but isn't quite there yet.
@@ -26,7 +26,9 @@
                  , Text.Ginger.Parse
                  , Text.Ginger.Run
                  , Text.PrintfA
-  -- other-modules:
+  other-modules: Text.Ginger.Run.Type
+               , Text.Ginger.Run.FuncUtils
+               , Text.Ginger.Run.Builtins
   -- other-extensions:
   build-depends: base >=4.5 && <5
                , aeson
diff --git a/src/Text/Ginger/GVal.hs b/src/Text/Ginger/GVal.hs
--- a/src/Text/Ginger/GVal.hs
+++ b/src/Text/Ginger/GVal.hs
@@ -35,8 +35,7 @@
                , Monad
                )
 import qualified Prelude
-import qualified Data.List as List
-import Data.Maybe ( fromMaybe, catMaybes, isJust )
+import Data.Maybe ( fromMaybe, catMaybes, isJust, mapMaybe )
 import Data.Text (Text)
 import Data.String (IsString, fromString)
 import qualified Data.Text as Text
@@ -129,11 +128,11 @@
     toJSON g =
         if isNull g
             then JSON.Null
-            else (fromMaybe (JSON.toJSON $ asText g) $
+            else fromMaybe (JSON.toJSON $ asText g) $
                     asJSON g <|>
                     (JSON.toJSON <$> asList g) <|>
                     (JSON.toJSON <$> asHashMap g) <|>
-                    (JSON.toJSON <$> asNumber g))
+                    (JSON.toJSON <$> asNumber g)
 
 -- | For convenience, 'Show' is implemented in a way that looks similar to
 -- JavaScript / JSON
@@ -167,7 +166,7 @@
             'c' -> formatString
                     (Text.unpack $ asText x)
                     fmt
-            f -> if f `Prelude.elem` ("fFgGeE" :: [Char])
+            f -> if f `Prelude.elem` ['f', 'F', 'g', 'G', 'e', 'E']
                     then formatRealFloat (toRealFloat . fromMaybe 0 . asNumber $ x) fmt
                     else formatInteger (Prelude.round . fromMaybe 0 . asNumber $ x) fmt
 
@@ -192,7 +191,7 @@
         numPositional = Prelude.length fromPositional
         namesRemaining = Prelude.drop numPositional names
         positional = Prelude.drop numPositional positionalRaw
-        fromNamed = catMaybes $ (List.map lookupName namesRemaining)
+        fromNamed = mapMaybe lookupName namesRemaining
         lookupName n = do
             v <- HashMap.lookup n namedRaw
             return (n, v)
@@ -240,7 +239,7 @@
                     , asText = mconcat . Prelude.map asText . HashMap.elems $ xs
                     , asBoolean = not . HashMap.null $ xs
                     , isNull = False
-                    , asLookup = Just (\v -> HashMap.lookup v xs)
+                    , asLookup = Just (`HashMap.lookup` xs)
                     , asDictItems = Just $ HashMap.toList xs
                     }
 
@@ -305,11 +304,11 @@
 orderedDict :: [Pair m] -> GVal m
 orderedDict xs =
     def
-        { asHtml = mconcat . Prelude.map asHtml . Prelude.map snd $ xs
-        , asText = mconcat . Prelude.map asText . Prelude.map snd $ xs
+        { asHtml = mconcat . Prelude.map (asHtml . snd) $ xs
+        , asText = mconcat . Prelude.map (asText . snd) $ xs
         , asBoolean = not . Prelude.null $ xs
         , isNull = False
-        , asLookup = Just (\v -> HashMap.lookup v hm)
+        , asLookup = Just (`HashMap.lookup` hm)
         , asDictItems = Just xs
         }
     where
@@ -413,7 +412,7 @@
 rawJSONToGVal (JSON.Number n) = toGVal n
 rawJSONToGVal (JSON.String s) = toGVal s
 rawJSONToGVal (JSON.Bool b) = toGVal b
-rawJSONToGVal (JSON.Null) = def
+rawJSONToGVal JSON.Null = def
 rawJSONToGVal (JSON.Array a) = toGVal $ Vector.toList a
 rawJSONToGVal (JSON.Object o) = toGVal o
 
diff --git a/src/Text/Ginger/Optimizer.hs b/src/Text/Ginger/Optimizer.hs
--- a/src/Text/Ginger/Optimizer.hs
+++ b/src/Text/Ginger/Optimizer.hs
@@ -4,7 +4,10 @@
 where
 
 import Text.Ginger.AST
+import Text.Ginger.GVal
 import Data.Monoid
+import Control.Monad.Identity
+import Data.Default
 
 class Optimizable a where
     optimize :: a -> a
@@ -21,10 +24,13 @@
 instance Optimizable Macro where
     optimize = optimizeMacro
 
+instance Optimizable Expression where
+    optimize = optimizeExpression
+
 optimizeTemplate t =
     t { templateBody = optimize $ templateBody t
-      , templateBlocks = fmap optimize $ templateBlocks t
-      , templateParent = fmap optimize $ templateParent t
+      , templateBlocks = optimize <$> templateBlocks t
+      , templateParent = optimize <$> templateParent t
       }
 
 {-
@@ -45,6 +51,15 @@
         [] -> NullS
         [x] -> x
         xs -> MultiS xs
+optimizeStatement (InterpolationS e) =
+    InterpolationS (optimize e)
+optimizeStatement s@(IfS c t f) =
+    let c' = optimize c
+    in case compileTimeEval c' of
+        Just gv -> case asBoolean gv of
+            True -> t
+            False -> f
+        _ -> s
 optimizeStatement s = s
 
 optimizeBlock (Block b) = Block $ optimize b
@@ -67,3 +82,56 @@
 mergeLiterals [x] = [x]
 mergeLiterals (x@(LiteralS a):y@(LiteralS b):xs) = mergeLiterals $ (LiteralS $ a <> b):xs
 mergeLiterals (x:xs) = x:mergeLiterals xs
+
+
+{-
+data Expression
+    = StringLiteralE Text -- ^ String literal expression: "foobar"
+    | NumberLiteralE Scientific -- ^ Numeric literal expression: 123.4
+    | BoolLiteralE Bool -- ^ Boolean literal expression: true
+    | NullLiteralE -- ^ Literal null
+    | VarE VarName -- ^ Variable reference: foobar
+    | ListE [Expression] -- ^ List construct: [ expr, expr, expr ]
+    | ObjectE [(Expression, Expression)] -- ^ Object construct: { expr: expr, expr: expr, ... }
+    | MemberLookupE Expression Expression -- ^ foo[bar] (also dot access)
+    | CallE Expression [(Maybe Text, Expression)] -- ^ foo(bar=baz, quux)
+    | LambdaE [Text] Expression -- ^ (foo, bar) -> expr
+    | TernaryE Expression Expression Expression -- ^ expr ? expr : expr
+    deriving (Show)
+-}
+
+optimizeExpression :: Expression -> Expression
+optimizeExpression = expandConstExpressions . optimizeSubexpressions
+
+expandConstExpressions :: Expression -> Expression
+expandConstExpressions e@(TernaryE c t f) =
+    case compileTimeEval c of
+        Just gv -> case asBoolean gv of
+            True -> t
+            False -> f
+        _ -> e
+expandConstExpressions e = e
+
+optimizeSubexpressions (ListE xs) = ListE (map optimize xs)
+optimizeSubexpressions (ObjectE xs) = ObjectE [ (optimize k, optimize v) | (k, v) <- xs ]
+optimizeSubexpressions (MemberLookupE k m) = MemberLookupE (optimize k) (optimize m)
+optimizeSubexpressions (CallE f args) = CallE (optimize f) [(n, optimize v) | (n, v) <- args]
+optimizeSubexpressions (LambdaE args body) = LambdaE args (optimize body)
+optimizeSubexpressions (TernaryE c t f) = TernaryE (optimize c) (optimize t) (optimize f)
+optimizeSubexpressions e = e
+
+isConstExpression :: Expression -> Bool
+isConstExpression (StringLiteralE _) = True
+isConstExpression (BoolLiteralE _) = True
+isConstExpression NullLiteralE = True
+isConstExpression (ListE xs) = all isConstExpression xs
+isConstExpression (ObjectE xs) = all (\(k,v) -> isConstExpression k && isConstExpression v) xs
+isConstExpression (MemberLookupE k m) = isConstExpression k && isConstExpression m
+isConstExpression e = False
+
+compileTimeEval :: Expression -> Maybe (GVal Identity)
+compileTimeEval (StringLiteralE s) = Just . toGVal $ s
+compileTimeEval (NumberLiteralE n) = Just . toGVal $ n
+compileTimeEval (BoolLiteralE b) = Just . toGVal $ b
+compileTimeEval NullLiteralE = Just def
+compileTimeEval e = Nothing
diff --git a/src/Text/Ginger/Parse.hs b/src/Text/Ginger/Parse.hs
--- a/src/Text/Ginger/Parse.hs
+++ b/src/Text/Ginger/Parse.hs
@@ -158,7 +158,7 @@
 
 reduceStatements :: [Statement] -> Statement
 reduceStatements [] = NullS
-reduceStatements (x:[]) = x
+reduceStatements [x] = x
 reduceStatements xs = MultiS xs
 
 templateP :: Monad m => Parser m Template
@@ -169,16 +169,16 @@
     parentName <- try (spaces >> fancyTagP "extends" stringLiteralP)
     parentTemplate <- includeTemplate parentName
     blocks <- HashMap.fromList <$> many blockP
-    return $ Template { templateBody = NullS, templateParent = Just parentTemplate, templateBlocks = blocks }
+    return Template { templateBody = NullS, templateParent = Just parentTemplate, templateBlocks = blocks }
 
 baseTemplateP :: Monad m => Parser m Template
 baseTemplateP = do
     body <- statementsP
     blocks <- psBlocks <$> getState
-    return $ Template { templateBody = body, templateParent = Nothing, templateBlocks = blocks }
+    return Template { templateBody = body, templateParent = Nothing, templateBlocks = blocks }
 
 statementsP :: Monad m => Parser m Statement
-statementsP = do
+statementsP =
     reduceStatements . filter (not . isNullS) <$> many (try statementP)
     where
         isNullS NullS = True
@@ -199,7 +199,7 @@
 
 interpolationStmtP :: Monad m => Parser m Statement
 interpolationStmtP = do
-    try $ openInterpolationP
+    try openInterpolationP
     spaces
     expr <- expressionP
     spaces
@@ -223,8 +223,8 @@
 
 commentStmtP :: Monad m => Parser m Statement
 commentStmtP = do
-    try $ openCommentP
-    manyTill anyChar (try $ closeCommentP)
+    try openCommentP
+    manyTill anyChar (try closeCommentP)
     return NullS
 
 ifStmtP :: Monad m => Parser m Statement
@@ -340,7 +340,7 @@
     let forLoop = ForS varNameIndex varNameVal iteree body
     return $ maybe
         forLoop
-        (\elseBranch -> IfS iteree forLoop elseBranch)
+        (IfS iteree forLoop)
         elseBranchMay
 
 includeP :: Monad m => Parser m Statement
@@ -392,14 +392,13 @@
     return (iteree, varIdent, indexIdent)
 
 fancyTagP :: Monad m => String -> Parser m a -> Parser m a
-fancyTagP tagName inner =
+fancyTagP tagName =
     between
         (try $ do
             openTagP
             string tagName
             spaces)
         closeTagP
-        inner
 
 simpleTagP :: Monad m => String -> Parser m ()
 simpleTagP tagName = openTagP >> string tagName >> closeTagP
@@ -476,12 +475,12 @@
     return $ foldl (flip ($)) lhs tails
     where
         opChars :: [Char]
-        opChars = nub . sort . concat . map fst $ operators
+        opChars = nub . sort . concatMap fst $ operators
         operativeTail :: Parser m (Expression -> Expression)
         operativeTail = do
             funcName <-
-                foldl (<|>) (fail "operator") $
-                [ try (string op >> notFollowedBy (oneOf opChars)) >> return fn | (op, fn) <- operators ]
+                foldl (<|>) (fail "operator")
+                    [ try (string op >> notFollowedBy (oneOf opChars)) >> return fn | (op, fn) <- operators ]
             spaces
             rhs <- operandP
             spaces
@@ -589,7 +588,7 @@
 
 namedFuncArgP :: Monad m => Parser m (Maybe Text, Expression)
 namedFuncArgP = do
-    name <- try $ identifierP `before` (between spaces spaces $ string "=")
+    name <- try $ identifierP `before` between spaces spaces (string "=")
     expr <- expressionP
     return (Just name, expr)
 
@@ -667,7 +666,7 @@
 identCharP = oneOf (['a'..'z'] ++ ['A'..'Z'] ++ ['_'] ++ ['0'..'9'])
 
 stringLiteralExprP :: Monad m => Parser m Expression
-stringLiteralExprP = do
+stringLiteralExprP =
     StringLiteralE . Text.pack <$> stringLiteralP
 
 stringLiteralP :: Monad m => Parser m String
@@ -707,7 +706,7 @@
     return $ sign ++ integral ++ fractional
 
 followedBy :: Monad m => m b -> m a -> m a
-followedBy b a = a >>= \x -> (b >> return x)
+followedBy b a = a >>= \x -> b >> return x
 
 before :: Monad m => m a -> m b -> m a
 before = flip followedBy
diff --git a/src/Text/Ginger/Run.hs b/src/Text/Ginger/Run.hs
--- a/src/Text/Ginger/Run.hs
+++ b/src/Text/Ginger/Run.hs
@@ -56,14 +56,16 @@
                , id
                )
 import qualified Prelude
-import Data.Maybe (fromMaybe, isJust)
+import Data.Maybe (fromMaybe, isJust, isNothing)
 import qualified Data.List as List
 import Text.Ginger.AST
 import Text.Ginger.Html
 import Text.Ginger.GVal
+import Text.Ginger.Run.Type
+import Text.Ginger.Run.Builtins
+import Text.Ginger.Run.FuncUtils
 import Text.Printf
 import Text.PrintfA
-import Data.Scientific (formatScientific)
 
 import Data.Text (Text)
 import Data.String (fromString)
@@ -77,549 +79,59 @@
 import Control.Applicative
 import qualified Data.HashMap.Strict as HashMap
 import Data.HashMap.Strict (HashMap)
-import Data.Scientific (Scientific)
-import Data.Scientific as Scientific
+import Data.Scientific (Scientific, formatScientific)
+import qualified Data.Scientific as Scientific
 import Data.Default (def)
 import Safe (readMay, lastDef, headMay)
 import Network.HTTP.Types (urlEncode)
 import Debug.Trace (trace)
-import Data.Maybe (isNothing)
 import Data.List (lookup, zipWith, unzip)
 
--- | Execution context. Determines how to look up variables from the
--- environment, and how to write out template output.
-data GingerContext m h
-    = GingerContext
-        { contextLookup :: VarName -> Run m h (GVal (Run m h))
-        , contextWrite :: h -> Run m h ()
-        , contextEncode :: GVal (Run m h) -> h
-        }
-
-contextWriteEncoded :: GingerContext m h -> GVal (Run m h) -> Run m h ()
-contextWriteEncoded context =
-    contextWrite context . contextEncode context
-
-data RunState m h
-    = RunState
-        { rsScope :: HashMap VarName (GVal (Run m h))
-        , rsCapture :: h
-        , rsCurrentTemplate :: Template -- the template we are currently running
-        , rsCurrentBlockName :: Maybe Text -- the name of the innermost block we're currently in
-        }
-
-unaryFunc :: forall m h. (Monad m) => (GVal (Run m h) -> GVal (Run m h)) -> Function (Run m h)
-unaryFunc f [] = return def
-unaryFunc f ((_, x):[]) = return (f x)
-
-ignoreArgNames :: ([a] -> b) -> ([(c, a)] -> b)
-ignoreArgNames f args = f (Prelude.map snd args)
-
-variadicNumericFunc :: Monad m => Scientific -> ([Scientific] -> Scientific) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
-variadicNumericFunc zero f args =
-    return . toGVal . f $ args'
-    where
-        args' :: [Scientific]
-        args' = Prelude.map (fromMaybe zero . asNumber . snd) args
-
-unaryNumericFunc :: Monad m => Scientific -> (Scientific -> Scientific) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
-unaryNumericFunc zero f args =
-    return . toGVal . f $ args'
-    where
-        args' :: Scientific
-        args' = case args of
-                    [] -> 0
-                    (arg:_) -> fromMaybe zero . asNumber . snd $ arg
-
-variadicStringFunc :: Monad m => ([Text] -> Text) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
-variadicStringFunc f args =
-    return . toGVal . f $ args'
-    where
-        args' :: [Text]
-        args' = Prelude.map (asText . snd) args
-
--- | Match args according to a given arg spec, Python style.
--- The return value is a triple of @(matched, args, kwargs, unmatchedNames)@,
--- where @matches@ is a hash map of named captured arguments, args is a list of
--- remaining unmatched positional arguments, kwargs is a list of remaining
--- unmatched named arguments, and @unmatchedNames@ contains the argument names
--- that haven't been matched.
-extractArgs :: [Text] -> [(Maybe Text, a)] -> (HashMap Text a, [a], HashMap Text a, [Text])
-extractArgs argNames args =
-    let (matchedPositional, argNames', args') = matchPositionalArgs argNames args
-        (matchedKeyword, argNames'', args'') = matchKeywordArgs argNames' args'
-        unmatchedPositional = [ a | (Nothing, a) <- args'' ]
-        unmatchedKeyword = HashMap.fromList [ (k, v) | (Just k, v) <- args'' ]
-    in ( HashMap.fromList (matchedPositional ++ matchedKeyword)
-       , unmatchedPositional
-       , unmatchedKeyword
-       , argNames''
-       )
-    where
-        matchPositionalArgs :: [Text] -> [(Maybe Text, a)] -> ([(Text, a)], [Text], [(Maybe Text, a)])
-        matchPositionalArgs [] args = ([], [], args)
-        matchPositionalArgs names [] = ([], names, [])
-        matchPositionalArgs names@(n:ns) allArgs@((anm, arg):args)
-            | Just n == anm || isNothing anm =
-                let (matched, ns', args') = matchPositionalArgs ns args
-                in ((n, arg):matched, ns', args')
-            | otherwise = ([], names, allArgs)
-
-        matchKeywordArgs :: [Text] -> [(Maybe Text, a)] -> ([(Text, a)], [Text], [(Maybe Text, a)])
-        matchKeywordArgs [] args = ([], [], args)
-        matchKeywordArgs names allArgs@((Nothing, arg):args) =
-            let (matched, ns', args') = matchKeywordArgs names args
-            in (matched, ns', (Nothing, arg):args')
-        matchKeywordArgs names@(n:ns) args =
-            case (lookup (Just n) args) of
-                Nothing ->
-                    let (matched, ns', args') = matchKeywordArgs ns args
-                    in (matched, n:ns', args')
-                Just v ->
-                    let args' = [ (k,v) | (k,v) <- args, k /= Just n ]
-                        (matched, ns', args'') = matchKeywordArgs ns args'
-                    in ((n,v):matched, ns', args'')
-
--- | Parse argument list into type-safe argument structure.
-extractArgsT :: ([Maybe a] -> b) -> [Text] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) b
-extractArgsT f argNames args =
-    let (matchedMap, freeArgs, freeKwargs, unmatched) = extractArgs argNames args
-    in if List.null freeArgs && HashMap.null freeKwargs
-        then Right (f $ fmap (\name -> HashMap.lookup name matchedMap) argNames)
-        else Left (freeArgs, freeKwargs, unmatched)
-
--- | Parse argument list into flat list of matched arguments.
-extractArgsL :: [Text] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) [Maybe a]
-extractArgsL = extractArgsT id
-
-extractArgsDefL :: [(Text, a)] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) [a]
-extractArgsDefL argSpec args =
-    let (names, defs) = unzip argSpec
-    in injectDefaults defs <$> extractArgsL names args
-
-injectDefaults :: [a] -> [Maybe a] -> [a]
-injectDefaults = zipWith fromMaybe
-
-defRunState :: forall m h. (Monoid h, Monad m) => Template -> RunState m h
-defRunState tpl =
-    RunState
-        { rsScope = HashMap.fromList scope
-        , rsCapture = mempty
-        , rsCurrentTemplate = tpl
-        , rsCurrentBlockName = Nothing
-        }
-    where
-        scope :: [(Text, GVal (Run m h))]
-        scope =
-            [ ("raw", fromFunction gfnRawHtml)
-            , ("abs", fromFunction . unaryNumericFunc 0 $ Prelude.abs)
-            , ("any", fromFunction gfnAny)
-            , ("all", fromFunction gfnAll)
-            -- TODO: batch
-            , ("capitalize", fromFunction . variadicStringFunc $ mconcat . Prelude.map capitalize)
-            , ("ceil", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.ceiling)
-            , ("center", fromFunction gfnCenter)
-            , ("concat", fromFunction . variadicStringFunc $ mconcat)
-            , ("contains", fromFunction gfnContains)
-            , ("d", fromFunction gfnDefault)
-            , ("default", fromFunction gfnDefault)
-            , ("difference", fromFunction . variadicNumericFunc 0 $ difference)
-            , ("e", fromFunction gfnEscape)
-            , ("equals", fromFunction gfnEquals)
-            , ("escape", fromFunction gfnEscape)
-            , ("filesizeformat", fromFunction gfnFileSizeFormat)
-            , ("filter", fromFunction gfnFilter)
-            , ("floor", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.floor)
-            , ("greater", fromFunction gfnGreater)
-            , ("greaterEquals", fromFunction gfnGreaterEquals)
-            , ("int", fromFunction . unaryFunc $ toGVal . (fmap (Prelude.truncate :: Scientific -> Int)) . asNumber)
-            , ("int_ratio", fromFunction . variadicNumericFunc 1 $ fromIntegral . intRatio . Prelude.map Prelude.floor)
-            , ("iterable", fromFunction . unaryFunc $ toGVal . (\x -> isList x || isDict x))
-            , ("length", fromFunction . unaryFunc $ toGVal . length)
-            , ("less", fromFunction gfnLess)
-            , ("lessEquals", fromFunction gfnLessEquals)
-            , ("modulo", fromFunction . variadicNumericFunc 1 $ fromIntegral . modulo . Prelude.map Prelude.floor)
-            , ("nequals", fromFunction gfnNEquals)
-            , ("num", fromFunction . unaryFunc $ toGVal . asNumber)
-            , ("printf", fromFunction gfnPrintf)
-            , ("product", fromFunction . variadicNumericFunc 1 $ Prelude.product)
-            , ("ratio", fromFunction . variadicNumericFunc 1 $ Scientific.fromFloatDigits . ratio . Prelude.map Scientific.toRealFloat)
-            , ("replace", fromFunction $ gfnReplace)
-            , ("round", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.round)
-            , ("show", fromFunction . unaryFunc $ fromString . show)
-            , ("slice", fromFunction $ gfnSlice)
-            , ("sort", fromFunction $ gfnSort)
-            , ("str", fromFunction . unaryFunc $ toGVal . asText)
-            , ("sum", fromFunction . variadicNumericFunc 0 $ Prelude.sum)
-            , ("truncate", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.truncate)
-            , ("urlencode", fromFunction $ gfnUrlEncode)
-            ]
-
-        gfnRawHtml :: Function (Run m h)
-        gfnRawHtml = unaryFunc (toGVal . unsafeRawHtml . asText)
-
-        gfnUrlEncode :: Function (Run m h)
-        gfnUrlEncode =
-            unaryFunc
-                ( toGVal
-                . Text.pack
-                . UTF8.toString
-                . urlEncode True
-                . UTF8.fromString
-                . Text.unpack
-                . asText
-                )
-
-        gfnDefault :: Function (Run m h)
-        gfnDefault [] = return def
-        gfnDefault ((_, x):xs)
-            | asBoolean x = return x
-            | otherwise = gfnDefault xs
-
-        gfnEscape :: Function (Run m h)
-        gfnEscape = return . toGVal . html . mconcat . fmap (asText . snd)
-
-        gfnAny :: Function (Run m h)
-        gfnAny xs = return . toGVal $ Prelude.any (asBoolean . snd) xs
-
-        gfnAll :: Function (Run m h)
-        gfnAll xs = return . toGVal $ Prelude.all (asBoolean . snd) xs
-
-        gfnEquals :: Function (Run m h)
-        gfnEquals [] = return $ toGVal True
-        gfnEquals (x:[]) = return $ toGVal True
-        gfnEquals (x:xs) =
-            return . toGVal $ Prelude.all ((snd x `looseEquals`) . snd) xs
-
-        gfnNEquals :: Function (Run m h)
-        gfnNEquals [] = return $ toGVal True
-        gfnNEquals (x:[]) = return $ toGVal True
-        gfnNEquals (x:xs) =
-            return . toGVal $ Prelude.any (not . (snd x `looseEquals`) . snd) xs
-
-        gfnContains :: Function (Run m h)
-        gfnContains [] = return $ toGVal False
-        gfnContains (list:elems) =
-            let rawList = fromMaybe [] . asList . snd $ list
-                rawElems = fmap snd elems
-                e `isInList` xs = Prelude.any (looseEquals e) xs
-                es `areInList` xs = Prelude.all (`isInList` xs) es
-            in return . toGVal $ rawElems `areInList` rawList
-
-        looseEquals :: GVal (Run m h) -> GVal (Run m h) -> Bool
-        looseEquals a b
-            | isJust (asFunction a) || isJust (asFunction b) = False
-            | isJust (asList a) /= isJust (asList b) = False
-            | isJust (asDictItems a) /= isJust (asDictItems b) = False
-            -- Both numbers: do numeric comparison
-            | isJust (asNumber a) && isJust (asNumber b) = asNumber a == asNumber b
-            -- If either is NULL, the other must be falsy
-            | isNull a || isNull b = asBoolean a == asBoolean b
-            | otherwise = asText a == asText b
-
-        gfnLess :: Function (Run m h)
-        gfnLess [] = return . toGVal $ False
-        gfnLess xs' =
-            let xs = fmap snd xs'
-            in return . toGVal $
-                Prelude.all (== Just True) (Prelude.zipWith less xs (Prelude.tail xs))
-
-        gfnGreater :: Function (Run m h)
-        gfnGreater [] = return . toGVal $ False
-        gfnGreater xs' =
-            let xs = fmap snd xs'
-            in return . toGVal $
-                Prelude.all (== Just True) (Prelude.zipWith greater xs (Prelude.tail xs))
-
-        gfnLessEquals :: Function (Run m h)
-        gfnLessEquals [] = return . toGVal $ False
-        gfnLessEquals xs' =
-            let xs = fmap snd xs'
-            in return . toGVal $
-                Prelude.all (== Just True) (Prelude.zipWith lessEq xs (Prelude.tail xs))
-
-        gfnGreaterEquals :: Function (Run m h)
-        gfnGreaterEquals [] = return . toGVal $ False
-        gfnGreaterEquals xs' =
-            let xs = fmap snd xs'
-            in return . toGVal $
-                Prelude.all (== Just True) (Prelude.zipWith greaterEq xs (Prelude.tail xs))
-
-        less :: GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
-        less a b = (<) <$> asNumber a <*> asNumber b
-
-        greater :: GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
-        greater a b = (>) <$> asNumber a <*> asNumber b
-
-        lessEq :: GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
-        lessEq a b = (<=) <$> asNumber a <*> asNumber b
-
-        greaterEq :: GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
-        greaterEq a b = (>=) <$> asNumber a <*> asNumber b
-
-        difference :: Prelude.Num a => [a] -> a
-        difference (x:xs) = x - Prelude.sum xs
-        difference [] = 0
-
-        ratio :: (Show a, Prelude.Fractional a, Prelude.Num a) => [a] -> a
-        ratio (x:xs) = x / Prelude.product xs
-        ratio [] = 0
-
-        intRatio :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
-        intRatio (x:xs) = x `Prelude.div` Prelude.product xs
-        intRatio [] = 0
-
-        modulo :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
-        modulo (x:xs) = x `Prelude.mod` Prelude.product xs
-        modulo [] = 0
-
-        capitalize :: Text -> Text
-        capitalize txt = Text.toUpper (Text.take 1 txt) <> Text.drop 1 txt
-
-        gfnCenter :: Function (Run m h)
-        gfnCenter [] = gfnCenter [(Nothing, toGVal ("" :: Text))]
-        gfnCenter (x:[]) = gfnCenter [x, (Nothing, toGVal (80 :: Int))]
-        gfnCenter (x:y:[]) = gfnCenter [x, y, (Nothing, toGVal (" " :: Text))]
-        gfnCenter ((_, s):(_, w):(_, pad):_) =
-            return . toGVal $ center (asText s) (fromMaybe 80 $ Prelude.truncate <$> asNumber w) (asText pad)
-
-        gfnSlice :: Function (Run m h)
-        gfnSlice args =
-            let argValues =
-                    extractArgsDefL
-                        [ ("slicee", def)
-                        , ("start", def)
-                        , ("length", def)
-                        ]
-                        args
-            in case argValues of
-                Right (slicee:startPos:length:[]) -> do
-                    let startInt :: Int
-                        startInt = fromMaybe 0 . fmap Prelude.round . asNumber $ startPos
-
-                        lengthInt :: Maybe Int
-                        lengthInt = fmap Prelude.round . asNumber $ length
-
-                        slice :: [a] -> Int -> Maybe Int -> [a]
-                        slice xs start Nothing =
-                            Prelude.drop start $ xs
-                        slice xs start (Just length) =
-                            Prelude.take length . Prelude.drop start $ xs
-                    case asDictItems slicee of
-                        Just items -> do
-                            let slicedItems = slice items startInt lengthInt
-                            return $ dict slicedItems
-                        Nothing -> do
-                            let items = fromMaybe [] $ asList slicee
-                                slicedItems = slice items startInt lengthInt
-                            return $ toGVal slicedItems
-                _ -> fail "Invalid arguments to 'slice'"
-
-        gfnReplace :: Function (Run m h)
-        gfnReplace args =
-            let argValues =
-                    extractArgsDefL
-                        [ ("str", def)
-                        , ("search", def)
-                        , ("replace", def)
-                        ]
-                        args
-            in case argValues of
-                Right (strG:searchG:replaceG:[]) -> do
-                    let str = asText strG
-                        search = asText searchG
-                        replace = asText replaceG
-                    return . toGVal $ Text.replace search replace str
-                _ -> fail "Invalid arguments to 'replace'"
-
-        gfnSort :: Function (Run m h)
-        gfnSort [] = return def
-        gfnSort ((_,sortee):args) = do
-            let sortKeyMay = asText <$> List.lookup (Just "by") args
-                sortReverse = fromMaybe False $ asBoolean <$> List.lookup (Just "reverse") args
-                baseComparer :: (GVal (Run m h)) -> (GVal (Run m h)) -> Prelude.Ordering
-                baseComparer = \a b -> Prelude.compare (asText a) (asText b)
-                extractKey :: Text -> GVal (Run m h) -> GVal (Run m h)
-                extractKey k g = fromMaybe def $ do
-                    l <- asLookup g
-                    l k
-            if isDict sortee
-                then do
-                    let comparer' :: (Text, GVal (Run m h)) -> (Text, GVal (Run m h)) -> Prelude.Ordering
-                        comparer' = case sortKeyMay of
-                            Nothing -> \(_, a) (_, b) -> baseComparer a b
-                            Just "__key" -> \(a, _) (b, _) -> Prelude.compare a b
-                            Just k -> \(_, a) (_, b) ->
-                                baseComparer
-                                    (extractKey k a) (extractKey k b)
-                        comparer =
-                            if sortReverse
-                                then \a b -> comparer' b a
-                                else comparer'
-                    return . toGVal $ List.sortBy comparer (fromMaybe [] $ asDictItems sortee)
-                else do
-                    let comparer' :: (GVal (Run m h)) -> (GVal (Run m h)) -> Prelude.Ordering
-                        comparer' = case sortKeyMay of
-                            Nothing ->
-                                baseComparer
-                            Just k -> \a b ->
-                                baseComparer
-                                    (extractKey k a) (extractKey k b)
-                    let comparer =
-                            if sortReverse
-                                then \a b -> comparer' b a
-                                else comparer'
-                    return . toGVal $ List.sortBy comparer (fromMaybe [] $ asList sortee)
-
-        center :: Text -> Prelude.Int -> Text -> Text
-        center str width pad =
-            if Text.length str Prelude.>= width
-                then str
-                else paddingL <> str <> paddingR
-            where
-                chars = width - Text.length str
-                charsL = chars `div` 2
-                charsR = chars - charsL
-                repsL = Prelude.succ charsL `div` Text.length pad
-                paddingL = Text.take charsL . Text.replicate repsL $ pad
-                repsR = Prelude.succ charsR `div` Text.length pad
-                paddingR = Text.take charsR . Text.replicate repsR $ pad
-
-        gfnFileSizeFormat :: Function (Run m h)
-        gfnFileSizeFormat [(_, sizeG)] =
-            gfnFileSizeFormat [(Nothing, sizeG), (Nothing, def)]
-        gfnFileSizeFormat [(_, sizeG), (_, binaryG)] = do
-            let sizeM = Prelude.round <$> asNumber sizeG
-                binary = asBoolean binaryG
-            Prelude.maybe
-                (return def)
-                (return . toGVal . formatFileSize binary)
-                sizeM
-        gfnFileSizeFormat _ = return def
-
-        formatFileSize :: Bool -> Integer -> String
-        formatFileSize binary size =
-            let units =
-                    if binary
-                        then
-                            [ (1, "B")
-                            , (1024, "kiB")
-                            , (1024 ^ 2, "MiB")
-                            , (1024 ^ 3, "GiB")
-                            , (1024 ^ 4, "TiB")
-                            , (1024 ^ 5, "PiB")
-                            ]
-                        else
-                            [ (1, "B")
-                            , (1000, "kB")
-                            , (1000000, "MB")
-                            , (1000000000, "GB")
-                            , (1000000000000, "TB")
-                            , (1000000000000000, "PB")
-                            ]
-                (divisor, unitName) =
-                    lastDef (1, "B") [ (d, u) | (d, u) <- units, d <= size ]
-                dividedSize :: Scientific
-                dividedSize = fromIntegral size / fromIntegral divisor
-                formattedSize =
-                    if isInteger dividedSize
-                        then formatScientific Fixed (Just 0) dividedSize
-                        else formatScientific Fixed (Just 1) dividedSize
-            in formattedSize ++ " " ++ unitName
-
-        gfnPrintf :: Function (Run m h)
-        gfnPrintf [] = return def
-        gfnPrintf [(_, fmtStrG)] = return fmtStrG
-        gfnPrintf ((_, fmtStrG):args) = do
-            return . toGVal $ printfG fmtStr (fmap snd args)
-            where
-                fmtStr = Text.unpack $ asText fmtStrG
-
-        gfnFilter :: Function (Run m h)
-        gfnFilter [] = return def
-        gfnFilter [(_, xs)] = return xs
-        gfnFilter ((_, xs):(_, p):args) = do
-            pfnG <- maybe (fail "Not a function") return (asFunction p)
-            let pfn x = asBoolean <$> pfnG ((Nothing, x):args)
-                xsl = fromMaybe [] (asList xs)
-            filtered <- filterM pfn xsl
-            return $ toGVal filtered
-
-printfG :: String -> [GVal m] -> String
-printfG fmt args = printfa fmt (fmap P args)
-
--- | Create an execution context for runGingerT.
--- Takes a lookup function, which returns ginger values into the carrier monad
--- based on a lookup key, and a writer function (outputting HTML by whatever
--- means the carrier monad provides, e.g. @putStr@ for @IO@, or @tell@ for
--- @Writer@s).
-makeContextM' :: (Monad m, Functor m)
-             => (VarName -> Run m h (GVal (Run m h)))
-             -> (h -> m ())
-             -> (GVal (Run m h) -> h)
-             -> GingerContext m h
-makeContextM' lookupFn writeFn encodeFn =
-    GingerContext
-        { contextLookup = lookupFn
-        , contextWrite = liftRun2 writeFn
-        , contextEncode = encodeFn
-        }
-
-liftLookup :: (Monad m, ToGVal (Run m h) v) => (VarName -> m v) -> VarName -> Run m h (GVal (Run m h))
-liftLookup f k = do
-    v <- liftRun $ f k
-    return . toGVal $ v
-
--- | Create an execution context for runGinger.
--- The argument is a lookup function that maps top-level context keys to ginger
--- values. 'makeContext' is a specialized version of 'makeContextM', targeting
--- the 'Writer' 'Html' monad (which is what is used for the non-monadic
--- template interpreter 'runGinger').
---
--- The type of the lookup function may look intimidating, but in most cases,
--- marshalling values from Haskell to Ginger is a matter of calling 'toGVal'
--- on them, so the 'GVal (Run (Writer Html))' part can usually be ignored.
--- See the 'Text.Ginger.GVal' module for details.
-makeContext' :: Monoid h
-            => (VarName -> GVal (Run (Writer h) h))
-            -> (GVal (Run (Writer h) h) -> h)
-            -> GingerContext (Writer h) h
-makeContext' lookupFn encodeFn =
-    makeContextM'
-        (return . lookupFn)
-        tell
-        encodeFn
-
-{-#DEPRECATED makeContext "Compatibility alias for makeContextHtml" #-}
-makeContext :: (VarName -> GVal (Run (Writer Html) Html))
-            -> GingerContext (Writer Html) Html
-makeContext = makeContextHtml
-
-{-#DEPRECATED makeContextM "Compatibility alias for makeContextHtmlM" #-}
-makeContextM :: (Monad m, Functor m)
-             => (VarName -> Run m Html (GVal (Run m Html)))
-             -> (Html -> m ())
-             -> GingerContext m Html
-makeContextM = makeContextHtmlM
-
-makeContextHtml :: (VarName -> GVal (Run (Writer Html) Html))
-                -> GingerContext (Writer Html) Html
-makeContextHtml l = makeContext' l toHtml
-
-makeContextHtmlM :: (Monad m, Functor m)
-                 => (VarName -> Run m Html (GVal (Run m Html)))
-                 -> (Html -> m ())
-                 -> GingerContext m Html
-makeContextHtmlM l w = makeContextM' l w toHtml
-
-makeContextText :: (VarName -> GVal (Run (Writer Text) Text))
-                -> GingerContext (Writer Text) Text
-makeContextText l = makeContext' l asText
-
-makeContextTextM :: (Monad m, Functor m)
-                 => (VarName -> Run m Text (GVal (Run m Text)))
-                 -> (Text -> m ())
-                 -> GingerContext m Text
-makeContextTextM l w = makeContextM' l w asText
+defaultScope :: forall m h. Monad m => [(Text, GVal (Run m h))]
+defaultScope =
+    [ ("raw", fromFunction gfnRawHtml)
+    , ("abs", fromFunction . unaryNumericFunc 0 $ Prelude.abs)
+    , ("any", fromFunction gfnAny)
+    , ("all", fromFunction gfnAll)
+    -- TODO: batch
+    , ("capitalize", fromFunction . variadicStringFunc $ mconcat . Prelude.map capitalize)
+    , ("ceil", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.ceiling)
+    , ("center", fromFunction gfnCenter)
+    , ("concat", fromFunction . variadicStringFunc $ mconcat)
+    , ("contains", fromFunction gfnContains)
+    , ("d", fromFunction gfnDefault)
+    , ("default", fromFunction gfnDefault)
+    , ("difference", fromFunction . variadicNumericFunc 0 $ difference)
+    , ("e", fromFunction gfnEscape)
+    , ("equals", fromFunction gfnEquals)
+    , ("escape", fromFunction gfnEscape)
+    , ("filesizeformat", fromFunction gfnFileSizeFormat)
+    , ("filter", fromFunction gfnFilter)
+    , ("floor", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.floor)
+    , ("greater", fromFunction gfnGreater)
+    , ("greaterEquals", fromFunction gfnGreaterEquals)
+    , ("int", fromFunction . unaryFunc $ toGVal . fmap (Prelude.truncate :: Scientific -> Int) . asNumber)
+    , ("int_ratio", fromFunction . variadicNumericFunc 1 $ fromIntegral . intRatio . Prelude.map Prelude.floor)
+    , ("iterable", fromFunction . unaryFunc $ toGVal . (\x -> isList x || isDict x))
+    , ("length", fromFunction . unaryFunc $ toGVal . length)
+    , ("less", fromFunction gfnLess)
+    , ("lessEquals", fromFunction gfnLessEquals)
+    , ("modulo", fromFunction . variadicNumericFunc 1 $ fromIntegral . modulo . Prelude.map Prelude.floor)
+    , ("nequals", fromFunction gfnNEquals)
+    , ("num", fromFunction . unaryFunc $ toGVal . asNumber)
+    , ("printf", fromFunction gfnPrintf)
+    , ("product", fromFunction . variadicNumericFunc 1 $ Prelude.product)
+    , ("ratio", fromFunction . variadicNumericFunc 1 $ Scientific.fromFloatDigits . ratio . Prelude.map Scientific.toRealFloat)
+    , ("replace", fromFunction gfnReplace)
+    , ("round", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.round)
+    , ("show", fromFunction . unaryFunc $ fromString . show)
+    , ("slice", fromFunction gfnSlice)
+    , ("sort", fromFunction gfnSort)
+    , ("str", fromFunction . unaryFunc $ toGVal . asText)
+    , ("sum", fromFunction . variadicNumericFunc 0 $ Prelude.sum)
+    , ("truncate", fromFunction . unaryNumericFunc 0 $ Prelude.fromIntegral . Prelude.truncate)
+    , ("urlencode", fromFunction gfnUrlEncode)
+    ]
 
 -- | Purely expand a Ginger template. The underlying carrier monad is 'Writer'
 -- 'h', which is used to collect the output and render it into a 'h'
@@ -631,17 +143,6 @@
 runGingerT :: (ToGVal (Run m h) h, Monoid h, Monad m, Functor m) => GingerContext m h -> Template -> m ()
 runGingerT context tpl = runReaderT (evalStateT (runTemplate tpl) (defRunState tpl)) context
 
--- | Internal type alias for our template-runner monad stack.
-type Run m h = StateT (RunState m h) (ReaderT (GingerContext m h) m)
-
--- | Lift a value from the host monad @m@ into the 'Run' monad.
-liftRun :: Monad m => m a -> Run m h a
-liftRun = lift . lift
-
--- | Lift a function from the host monad @m@ into the 'Run' monad.
-liftRun2 :: Monad m => (a -> m b) -> a -> Run m h b
-liftRun2 f x = liftRun $ f x
-
 -- | Find the effective base template of an inheritance chain
 baseTemplate :: Template -> Template
 baseTemplate t =
@@ -677,7 +178,7 @@
     tpl <- gets rsCurrentTemplate
     let blockMay = resolveBlock blockName tpl
     case blockMay of
-        Nothing -> fail $ "Block " <> (Text.unpack blockName) <> " not defined"
+        Nothing -> fail $ "Block " <> Text.unpack blockName <> " not defined"
         Just block -> return block
     where
         resolveBlock :: VarName -> Template -> Maybe Block
@@ -748,7 +249,7 @@
                              , "first" ~> (index == 0)
                              , "last" ~> (Prelude.succ index == numItems)
                              , "length" ~> numItems
-                             , "cycle" ~> (fromFunction $ cycle index)
+                             , "cycle" ~> fromFunction (cycle index)
                              ])
                              { asFunction = Just loop }
                     case varNameIndex of
@@ -775,7 +276,7 @@
         f args =
             withLocalState . local (\c -> c { contextWrite = appendCapture }) $ do
                 clearCapture
-                forM (HashMap.toList matchedArgs) (uncurry setVar)
+                forM_ (HashMap.toList matchedArgs) (uncurry setVar)
                 setVar "varargs" . toGVal $ positionalArgs
                 setVar "kwargs" . toGVal $ namedArgs
                 runStatement body
@@ -836,7 +337,7 @@
 runExpression (StringLiteralE str) = return . toGVal $ str
 runExpression (NumberLiteralE n) = return . toGVal $ n
 runExpression (BoolLiteralE b) = return . toGVal $ b
-runExpression (NullLiteralE) = return def
+runExpression NullLiteralE = return def
 runExpression (VarE key) = getVar key
 runExpression (ListE xs) = toGVal <$> forM xs runExpression
 runExpression (ObjectE xs) = do
@@ -858,8 +359,7 @@
         Just f -> f args
 runExpression (LambdaE argNames body) = do
     let fn args = withLocalScope $ do
-            forM (Prelude.zip argNames (fmap snd args)) $ \(argName, arg) ->
-                setVar argName arg
+            forM_ (Prelude.zip argNames (fmap snd args)) $ uncurry setVar
             runExpression body
     return $ fromFunction fn
 runExpression (TernaryE condition yes no) = do
@@ -874,3 +374,13 @@
     e <- asks contextEncode
     p <- asks contextWrite
     p . e $ src
+
+defRunState :: forall m h. (Monoid h, Monad m) => Template -> RunState m h
+defRunState tpl =
+    RunState
+        { rsScope = HashMap.fromList defaultScope
+        , rsCapture = mempty
+        , rsCurrentTemplate = tpl
+        , rsCurrentBlockName = Nothing
+        }
+
diff --git a/src/Text/Ginger/Run/Builtins.hs b/src/Text/Ginger/Run/Builtins.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Run/Builtins.hs
@@ -0,0 +1,366 @@
+{-#LANGUAGE FlexibleContexts #-}
+{-#LANGUAGE FlexibleInstances #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE TupleSections #-}
+{-#LANGUAGE TypeSynonymInstances #-}
+{-#LANGUAGE MultiParamTypeClasses #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+module Text.Ginger.Run.Builtins
+where
+
+import Prelude ( (.), ($), (==), (/=)
+               , (>), (<), (>=), (<=)
+               , (+), (-), (*), (/), div, (**), (^)
+               , (||), (&&)
+               , (++)
+               , Show, show
+               , undefined, otherwise
+               , Maybe (..)
+               , Bool (..)
+               , Int, Integer, String
+               , fromIntegral, floor, round
+               , not
+               , show
+               , uncurry
+               , seq
+               , fst, snd
+               , maybe
+               , Either (..)
+               , id
+               , flip
+               )
+import qualified Prelude
+import Data.Maybe (fromMaybe, isJust, isNothing)
+import qualified Data.List as List
+import Text.Ginger.AST
+import Text.Ginger.Html
+import Text.Ginger.GVal
+import Text.Ginger.Run.Type
+import Text.Ginger.Run.FuncUtils
+import Text.Printf
+import Text.PrintfA
+
+import Data.Text (Text)
+import Data.String (fromString)
+import qualified Data.Text as Text
+import qualified Data.ByteString.UTF8 as UTF8
+import Control.Monad
+import Control.Monad.Identity
+import Control.Monad.Writer
+import Control.Monad.Reader
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
+import Data.Scientific (Scientific, formatScientific, FPFormat (Fixed) )
+import qualified Data.Scientific as Scientific
+import Data.Default (def)
+import Safe (readMay, lastDef, headMay)
+import Network.HTTP.Types (urlEncode)
+import Debug.Trace (trace)
+import Data.List (lookup, zipWith, unzip)
+
+gfnRawHtml :: Monad m => Function (Run m h)
+gfnRawHtml = unaryFunc (toGVal . unsafeRawHtml . asText)
+
+gfnUrlEncode :: Monad m => Function (Run m h)
+gfnUrlEncode =
+    unaryFunc
+        ( toGVal
+        . Text.pack
+        . UTF8.toString
+        . urlEncode True
+        . UTF8.fromString
+        . Text.unpack
+        . asText
+        )
+
+gfnDefault :: Monad m => Function (Run m h)
+gfnDefault [] = return def
+gfnDefault ((_, x):xs)
+    | asBoolean x = return x
+    | otherwise = gfnDefault xs
+
+gfnEscape :: Monad m => Function (Run m h)
+gfnEscape = return . toGVal . html . mconcat . fmap (asText . snd)
+
+gfnAny :: Monad m => Function (Run m h)
+gfnAny xs = return . toGVal $ Prelude.any (asBoolean . snd) xs
+
+gfnAll :: Monad m => Function (Run m h)
+gfnAll xs = return . toGVal $ Prelude.all (asBoolean . snd) xs
+
+gfnEquals :: Monad m => Function (Run m h)
+gfnEquals [] = return $ toGVal True
+gfnEquals [x] = return $ toGVal True
+gfnEquals (x:xs) =
+    return . toGVal $ Prelude.all ((snd x `looseEquals`) . snd) xs
+
+gfnNEquals :: Monad m => Function (Run m h)
+gfnNEquals [] = return $ toGVal True
+gfnNEquals [x] = return $ toGVal True
+gfnNEquals (x:xs) =
+    return . toGVal $ Prelude.any (not . (snd x `looseEquals`) . snd) xs
+
+gfnContains :: Monad m => Function (Run m h)
+gfnContains [] = return $ toGVal False
+gfnContains (list:elems) =
+    let rawList = fromMaybe [] . asList . snd $ list
+        rawElems = fmap snd elems
+        e `isInList` xs = Prelude.any (looseEquals e) xs
+        es `areInList` xs = Prelude.all (`isInList` xs) es
+    in return . toGVal $ rawElems `areInList` rawList
+
+looseEquals :: GVal (Run m h) -> GVal (Run m h) -> Bool
+looseEquals a b
+    | isJust (asFunction a) || isJust (asFunction b) = False
+    | isJust (asList a) /= isJust (asList b) = False
+    | isJust (asDictItems a) /= isJust (asDictItems b) = False
+    -- Both numbers: do numeric comparison
+    | isJust (asNumber a) && isJust (asNumber b) = asNumber a == asNumber b
+    -- If either is NULL, the other must be falsy
+    | isNull a || isNull b = asBoolean a == asBoolean b
+    | otherwise = asText a == asText b
+
+gfnLess :: Monad m => Function (Run m h)
+gfnLess [] = return . toGVal $ False
+gfnLess xs' =
+    let xs = fmap snd xs'
+    in return . toGVal $
+        Prelude.all (== Just True) (Prelude.zipWith less xs (Prelude.tail xs))
+
+gfnGreater :: Monad m => Function (Run m h)
+gfnGreater [] = return . toGVal $ False
+gfnGreater xs' =
+    let xs = fmap snd xs'
+    in return . toGVal $
+        Prelude.all (== Just True) (Prelude.zipWith greater xs (Prelude.tail xs))
+
+gfnLessEquals :: Monad m => Function (Run m h)
+gfnLessEquals [] = return . toGVal $ False
+gfnLessEquals xs' =
+    let xs = fmap snd xs'
+    in return . toGVal $
+        Prelude.all (== Just True) (Prelude.zipWith lessEq xs (Prelude.tail xs))
+
+gfnGreaterEquals :: Monad m => Function (Run m h)
+gfnGreaterEquals [] = return . toGVal $ False
+gfnGreaterEquals xs' =
+    let xs = fmap snd xs'
+    in return . toGVal $
+        Prelude.all (== Just True) (Prelude.zipWith greaterEq xs (Prelude.tail xs))
+
+less :: Monad m => GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
+less a b = (<) <$> asNumber a <*> asNumber b
+
+greater :: Monad m => GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
+greater a b = (>) <$> asNumber a <*> asNumber b
+
+lessEq :: Monad m => GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
+lessEq a b = (<=) <$> asNumber a <*> asNumber b
+
+greaterEq :: Monad m => GVal (Run m h) -> GVal (Run m h) -> Maybe Bool
+greaterEq a b = (>=) <$> asNumber a <*> asNumber b
+
+difference :: Prelude.Num a => [a] -> a
+difference (x:xs) = x - Prelude.sum xs
+difference [] = 0
+
+ratio :: (Show a, Prelude.Fractional a, Prelude.Num a) => [a] -> a
+ratio (x:xs) = x / Prelude.product xs
+ratio [] = 0
+
+intRatio :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
+intRatio (x:xs) = x `Prelude.div` Prelude.product xs
+intRatio [] = 0
+
+modulo :: (Prelude.Integral a, Prelude.Num a) => [a] -> a
+modulo (x:xs) = x `Prelude.mod` Prelude.product xs
+modulo [] = 0
+
+capitalize :: Text -> Text
+capitalize txt = Text.toUpper (Text.take 1 txt) <> Text.drop 1 txt
+
+gfnCenter :: Monad m => Function (Run m h)
+gfnCenter [] = gfnCenter [(Nothing, toGVal ("" :: Text))]
+gfnCenter [x] = gfnCenter [x, (Nothing, toGVal (80 :: Int))]
+gfnCenter [x,y] = gfnCenter [x, y, (Nothing, toGVal (" " :: Text))]
+gfnCenter ((_, s):(_, w):(_, pad):_) =
+    return . toGVal $ center (asText s) (fromMaybe 80 $ Prelude.truncate <$> asNumber w) (asText pad)
+
+gfnSlice :: Monad m => Function (Run m h)
+gfnSlice args =
+    let argValues =
+            extractArgsDefL
+                [ ("slicee", def)
+                , ("start", def)
+                , ("length", def)
+                ]
+                args
+    in case argValues of
+        Right [slicee, startPos, length] -> do
+            let startInt :: Int
+                startInt = maybe 0 Prelude.round . asNumber $ startPos
+
+                lengthInt :: Maybe Int
+                lengthInt = fmap Prelude.round . asNumber $ length
+
+                slice :: [a] -> Int -> Maybe Int -> [a]
+                slice xs start Nothing =
+                    Prelude.drop start xs
+                slice xs start (Just length) =
+                    Prelude.take length . Prelude.drop start $ xs
+            case asDictItems slicee of
+                Just items -> do
+                    let slicedItems = slice items startInt lengthInt
+                    return $ dict slicedItems
+                Nothing -> do
+                    let items = fromMaybe [] $ asList slicee
+                        slicedItems = slice items startInt lengthInt
+                    return $ toGVal slicedItems
+        _ -> fail "Invalid arguments to 'slice'"
+
+gfnReplace :: Monad m => Function (Run m h)
+gfnReplace args =
+    let argValues =
+            extractArgsDefL
+                [ ("str", def)
+                , ("search", def)
+                , ("replace", def)
+                ]
+                args
+    in case argValues of
+        Right [strG, searchG, replaceG] -> do
+            let str = asText strG
+                search = asText searchG
+                replace = asText replaceG
+            return . toGVal $ Text.replace search replace str
+        _ -> fail "Invalid arguments to 'replace'"
+
+gfnSort :: Monad m => Function (Run m h)
+gfnSort args = do
+    let parsedArgs = extractArgsDefL
+            [ ("sortee", def)
+            , ("by", def)
+            , ("reverse", toGVal False)
+            ]
+            args
+    (sortee, sortKey, sortReverse) <- case parsedArgs of
+        Right [sortee, sortKeyG, reverseG] ->
+            return ( sortee
+                   , asText sortKeyG
+                   , asBoolean reverseG
+                   )
+        _ ->
+            fail "Invalid args to sort()"
+    let baseComparer :: GVal (Run m h) -> GVal (Run m h) -> Prelude.Ordering
+        baseComparer a b = Prelude.compare (asText a) (asText b)
+        extractKey :: Text -> GVal (Run m h) -> GVal (Run m h)
+        extractKey k g = fromMaybe def $ do
+            l <- asLookup g
+            l k
+    if isDict sortee
+        then do
+            let comparer' :: (Text, GVal (Run m h)) -> (Text, GVal (Run m h)) -> Prelude.Ordering
+                comparer' = case sortKey of
+                    "" -> \(_, a) (_, b) -> baseComparer a b
+                    "__key" -> \(a, _) (b, _) -> Prelude.compare a b
+                    k -> \(_, a) (_, b) ->
+                        baseComparer
+                            (extractKey k a) (extractKey k b)
+                comparer =
+                    if sortReverse
+                        then flip comparer'
+                        else comparer'
+            return . toGVal $ List.sortBy comparer (fromMaybe [] $ asDictItems sortee)
+        else do
+            let comparer' :: GVal (Run m h) -> GVal (Run m h) -> Prelude.Ordering
+                comparer' = case sortKey of
+                    "" ->
+                        baseComparer
+                    k -> \a b ->
+                        baseComparer
+                            (extractKey k a) (extractKey k b)
+            let comparer =
+                    if sortReverse
+                        then flip comparer'
+                        else comparer'
+            return . toGVal $ List.sortBy comparer (fromMaybe [] $ asList sortee)
+
+center :: Text -> Prelude.Int -> Text -> Text
+center str width pad =
+    if Text.length str Prelude.>= width
+        then str
+        else paddingL <> str <> paddingR
+    where
+        chars = width - Text.length str
+        charsL = chars `div` 2
+        charsR = chars - charsL
+        repsL = Prelude.succ charsL `div` Text.length pad
+        paddingL = Text.take charsL . Text.replicate repsL $ pad
+        repsR = Prelude.succ charsR `div` Text.length pad
+        paddingR = Text.take charsR . Text.replicate repsR $ pad
+
+gfnFileSizeFormat :: Monad m => Function (Run m h)
+gfnFileSizeFormat [(_, sizeG)] =
+    gfnFileSizeFormat [(Nothing, sizeG), (Nothing, def)]
+gfnFileSizeFormat [(_, sizeG), (_, binaryG)] = do
+    let sizeM = Prelude.round <$> asNumber sizeG
+        binary = asBoolean binaryG
+    Prelude.maybe
+        (return def)
+        (return . toGVal . formatFileSize binary)
+        sizeM
+gfnFileSizeFormat _ = return def
+
+formatFileSize :: Bool -> Integer -> String
+formatFileSize binary size =
+    let units =
+            if binary
+                then
+                    [ (1, "B")
+                    , (1024, "kiB")
+                    , (1024 ^ 2, "MiB")
+                    , (1024 ^ 3, "GiB")
+                    , (1024 ^ 4, "TiB")
+                    , (1024 ^ 5, "PiB")
+                    ]
+                else
+                    [ (1, "B")
+                    , (1000, "kB")
+                    , (1000000, "MB")
+                    , (1000000000, "GB")
+                    , (1000000000000, "TB")
+                    , (1000000000000000, "PB")
+                    ]
+        (divisor, unitName) =
+            lastDef (1, "B") [ (d, u) | (d, u) <- units, d <= size ]
+        dividedSize :: Scientific
+        dividedSize = fromIntegral size / fromIntegral divisor
+        formattedSize =
+            if Scientific.isInteger dividedSize
+                then formatScientific Fixed (Just 0) dividedSize
+                else formatScientific Fixed (Just 1) dividedSize
+    in formattedSize ++ " " ++ unitName
+
+gfnPrintf :: Monad m => Function (Run m h)
+gfnPrintf [] = return def
+gfnPrintf [(_, fmtStrG)] = return fmtStrG
+gfnPrintf ((_, fmtStrG):args) = do
+    return . toGVal $ printfG fmtStr (fmap snd args)
+    where
+        fmtStr = Text.unpack $ asText fmtStrG
+
+gfnFilter :: Monad m => Function (Run m h)
+gfnFilter [] = return def
+gfnFilter [(_, xs)] = return xs
+gfnFilter ((_, xs):(_, p):args) = do
+    pfnG <- maybe (fail "Not a function") return (asFunction p)
+    let pfn x = asBoolean <$> pfnG ((Nothing, x):args)
+        xsl = fromMaybe [] (asList xs)
+    filtered <- filterM pfn xsl
+    return $ toGVal filtered
+
+printfG :: String -> [GVal m] -> String
+printfG fmt args = printfa fmt (fmap P args)
+
diff --git a/src/Text/Ginger/Run/FuncUtils.hs b/src/Text/Ginger/Run/FuncUtils.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Run/FuncUtils.hs
@@ -0,0 +1,153 @@
+{-#LANGUAGE FlexibleContexts #-}
+{-#LANGUAGE FlexibleInstances #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE TupleSections #-}
+{-#LANGUAGE TypeSynonymInstances #-}
+{-#LANGUAGE MultiParamTypeClasses #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+module Text.Ginger.Run.FuncUtils
+where
+
+import Prelude ( (.), ($), (==), (/=)
+               , (>), (<), (>=), (<=)
+               , (+), (-), (*), (/), div, (**), (^)
+               , (||), (&&)
+               , (++)
+               , Show, show
+               , undefined, otherwise
+               , Maybe (..)
+               , Bool (..)
+               , Int, Integer, String
+               , fromIntegral, floor, round
+               , not
+               , show
+               , uncurry
+               , seq
+               , fst, snd
+               , maybe
+               , Either (..)
+               , id
+               )
+import qualified Prelude
+import Data.Maybe (fromMaybe, isJust)
+import qualified Data.List as List
+import Text.Ginger.AST
+import Text.Ginger.Html
+import Text.Ginger.GVal
+import Text.Ginger.Run.Type
+import Text.Printf
+import Text.PrintfA
+import Data.Scientific (formatScientific)
+
+import Data.Text (Text)
+import Data.String (fromString)
+import qualified Data.Text as Text
+import qualified Data.ByteString.UTF8 as UTF8
+import Control.Monad
+import Control.Monad.Identity
+import Control.Monad.Writer
+import Control.Monad.Reader
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
+import Data.Scientific (Scientific)
+import Data.Scientific as Scientific
+import Data.Default (def)
+import Safe (readMay, lastDef, headMay)
+import Network.HTTP.Types (urlEncode)
+import Debug.Trace (trace)
+import Data.Maybe (isNothing)
+import Data.List (lookup, zipWith, unzip)
+
+unaryFunc :: forall m h. (Monad m) => (GVal (Run m h) -> GVal (Run m h)) -> Function (Run m h)
+unaryFunc f [] = return def
+unaryFunc f ((_, x):[]) = return (f x)
+
+ignoreArgNames :: ([a] -> b) -> ([(c, a)] -> b)
+ignoreArgNames f args = f (Prelude.map snd args)
+
+variadicNumericFunc :: Monad m => Scientific -> ([Scientific] -> Scientific) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
+variadicNumericFunc zero f args =
+    return . toGVal . f $ args'
+    where
+        args' :: [Scientific]
+        args' = Prelude.map (fromMaybe zero . asNumber . snd) args
+
+unaryNumericFunc :: Monad m => Scientific -> (Scientific -> Scientific) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
+unaryNumericFunc zero f args =
+    return . toGVal . f $ args'
+    where
+        args' :: Scientific
+        args' = case args of
+                    [] -> 0
+                    (arg:_) -> fromMaybe zero . asNumber . snd $ arg
+
+variadicStringFunc :: Monad m => ([Text] -> Text) -> [(Maybe Text, GVal (Run m h))] -> Run m h (GVal (Run m h))
+variadicStringFunc f args =
+    return . toGVal . f $ args'
+    where
+        args' :: [Text]
+        args' = Prelude.map (asText . snd) args
+
+-- | Match args according to a given arg spec, Python style.
+-- The return value is a triple of @(matched, args, kwargs, unmatchedNames)@,
+-- where @matches@ is a hash map of named captured arguments, args is a list of
+-- remaining unmatched positional arguments, kwargs is a list of remaining
+-- unmatched named arguments, and @unmatchedNames@ contains the argument names
+-- that haven't been matched.
+extractArgs :: [Text] -> [(Maybe Text, a)] -> (HashMap Text a, [a], HashMap Text a, [Text])
+extractArgs argNames args =
+    let (matchedPositional, argNames', args') = matchPositionalArgs argNames args
+        (matchedKeyword, argNames'', args'') = matchKeywordArgs argNames' args'
+        unmatchedPositional = [ a | (Nothing, a) <- args'' ]
+        unmatchedKeyword = HashMap.fromList [ (k, v) | (Just k, v) <- args'' ]
+    in ( HashMap.fromList (matchedPositional ++ matchedKeyword)
+       , unmatchedPositional
+       , unmatchedKeyword
+       , argNames''
+       )
+    where
+        matchPositionalArgs :: [Text] -> [(Maybe Text, a)] -> ([(Text, a)], [Text], [(Maybe Text, a)])
+        matchPositionalArgs [] args = ([], [], args)
+        matchPositionalArgs names [] = ([], names, [])
+        matchPositionalArgs names@(n:ns) allArgs@((anm, arg):args)
+            | Just n == anm || isNothing anm =
+                let (matched, ns', args') = matchPositionalArgs ns args
+                in ((n, arg):matched, ns', args')
+            | otherwise = ([], names, allArgs)
+
+        matchKeywordArgs :: [Text] -> [(Maybe Text, a)] -> ([(Text, a)], [Text], [(Maybe Text, a)])
+        matchKeywordArgs [] args = ([], [], args)
+        matchKeywordArgs names allArgs@((Nothing, arg):args) =
+            let (matched, ns', args') = matchKeywordArgs names args
+            in (matched, ns', (Nothing, arg):args')
+        matchKeywordArgs names@(n:ns) args =
+            case (lookup (Just n) args) of
+                Nothing ->
+                    let (matched, ns', args') = matchKeywordArgs ns args
+                    in (matched, n:ns', args')
+                Just v ->
+                    let args' = [ (k,v) | (k,v) <- args, k /= Just n ]
+                        (matched, ns', args'') = matchKeywordArgs ns args'
+                    in ((n,v):matched, ns', args'')
+
+-- | Parse argument list into type-safe argument structure.
+extractArgsT :: ([Maybe a] -> b) -> [Text] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) b
+extractArgsT f argNames args =
+    let (matchedMap, freeArgs, freeKwargs, unmatched) = extractArgs argNames args
+    in if List.null freeArgs && HashMap.null freeKwargs
+        then Right (f $ fmap (\name -> HashMap.lookup name matchedMap) argNames)
+        else Left (freeArgs, freeKwargs, unmatched)
+
+-- | Parse argument list into flat list of matched arguments.
+extractArgsL :: [Text] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) [Maybe a]
+extractArgsL = extractArgsT id
+
+extractArgsDefL :: [(Text, a)] -> [(Maybe Text, a)] -> Either ([a], HashMap Text a, [Text]) [a]
+extractArgsDefL argSpec args =
+    let (names, defs) = unzip argSpec
+    in injectDefaults defs <$> extractArgsL names args
+
+injectDefaults :: [a] -> [Maybe a] -> [a]
+injectDefaults = zipWith fromMaybe
diff --git a/src/Text/Ginger/Run/Type.hs b/src/Text/Ginger/Run/Type.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Ginger/Run/Type.hs
@@ -0,0 +1,183 @@
+{-#LANGUAGE FlexibleContexts #-}
+{-#LANGUAGE FlexibleInstances #-}
+{-#LANGUAGE OverloadedStrings #-}
+{-#LANGUAGE TupleSections #-}
+{-#LANGUAGE TypeSynonymInstances #-}
+{-#LANGUAGE MultiParamTypeClasses #-}
+{-#LANGUAGE ScopedTypeVariables #-}
+module Text.Ginger.Run.Type
+( GingerContext (..)
+, makeContext
+, makeContextM
+, makeContext'
+, makeContextM'
+, makeContextHtml
+, makeContextHtmlM
+, makeContextText
+, makeContextTextM
+, liftRun
+, liftRun2
+, Run (..)
+, RunState (..)
+)
+where
+
+import Prelude ( (.), ($), (==), (/=)
+               , (>), (<), (>=), (<=)
+               , (+), (-), (*), (/), div, (**), (^)
+               , (||), (&&)
+               , (++)
+               , Show, show
+               , undefined, otherwise
+               , Maybe (..)
+               , Bool (..)
+               , Int, Integer, String
+               , fromIntegral, floor, round
+               , not
+               , show
+               , uncurry
+               , seq
+               , fst, snd
+               , maybe
+               , Either (..)
+               , id
+               )
+import qualified Prelude
+import Data.Maybe (fromMaybe, isJust)
+import qualified Data.List as List
+import Text.Ginger.AST
+import Text.Ginger.Html
+import Text.Ginger.GVal
+import Text.Printf
+import Text.PrintfA
+import Data.Scientific (formatScientific)
+
+import Data.Text (Text)
+import Data.String (fromString)
+import qualified Data.Text as Text
+import qualified Data.ByteString.UTF8 as UTF8
+import Control.Monad
+import Control.Monad.Identity
+import Control.Monad.Writer
+import Control.Monad.Reader
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.HashMap.Strict as HashMap
+import Data.HashMap.Strict (HashMap)
+import Data.Scientific (Scientific)
+import Data.Scientific as Scientific
+import Data.Default (def)
+import Safe (readMay, lastDef, headMay)
+import Network.HTTP.Types (urlEncode)
+import Debug.Trace (trace)
+import Data.Maybe (isNothing)
+import Data.List (lookup, zipWith, unzip)
+
+-- | Execution context. Determines how to look up variables from the
+-- environment, and how to write out template output.
+data GingerContext m h
+    = GingerContext
+        { contextLookup :: VarName -> Run m h (GVal (Run m h))
+        , contextWrite :: h -> Run m h ()
+        , contextEncode :: GVal (Run m h) -> h
+        }
+
+contextWriteEncoded :: GingerContext m h -> GVal (Run m h) -> Run m h ()
+contextWriteEncoded context =
+    contextWrite context . contextEncode context
+
+-- | Create an execution context for runGingerT.
+-- Takes a lookup function, which returns ginger values into the carrier monad
+-- based on a lookup key, and a writer function (outputting HTML by whatever
+-- means the carrier monad provides, e.g. @putStr@ for @IO@, or @tell@ for
+-- @Writer@s).
+makeContextM' :: (Monad m, Functor m)
+             => (VarName -> Run m h (GVal (Run m h)))
+             -> (h -> m ())
+             -> (GVal (Run m h) -> h)
+             -> GingerContext m h
+makeContextM' lookupFn writeFn encodeFn =
+    GingerContext
+        { contextLookup = lookupFn
+        , contextWrite = liftRun2 writeFn
+        , contextEncode = encodeFn
+        }
+
+liftLookup :: (Monad m, ToGVal (Run m h) v) => (VarName -> m v) -> VarName -> Run m h (GVal (Run m h))
+liftLookup f k = do
+    v <- liftRun $ f k
+    return . toGVal $ v
+
+-- | Create an execution context for runGinger.
+-- The argument is a lookup function that maps top-level context keys to ginger
+-- values. 'makeContext' is a specialized version of 'makeContextM', targeting
+-- the 'Writer' 'Html' monad (which is what is used for the non-monadic
+-- template interpreter 'runGinger').
+--
+-- The type of the lookup function may look intimidating, but in most cases,
+-- marshalling values from Haskell to Ginger is a matter of calling 'toGVal'
+-- on them, so the 'GVal (Run (Writer Html))' part can usually be ignored.
+-- See the 'Text.Ginger.GVal' module for details.
+makeContext' :: Monoid h
+            => (VarName -> GVal (Run (Writer h) h))
+            -> (GVal (Run (Writer h) h) -> h)
+            -> GingerContext (Writer h) h
+makeContext' lookupFn encodeFn =
+    makeContextM'
+        (return . lookupFn)
+        tell
+        encodeFn
+
+{-#DEPRECATED makeContext "Compatibility alias for makeContextHtml" #-}
+makeContext :: (VarName -> GVal (Run (Writer Html) Html))
+            -> GingerContext (Writer Html) Html
+makeContext = makeContextHtml
+
+{-#DEPRECATED makeContextM "Compatibility alias for makeContextHtmlM" #-}
+makeContextM :: (Monad m, Functor m)
+             => (VarName -> Run m Html (GVal (Run m Html)))
+             -> (Html -> m ())
+             -> GingerContext m Html
+makeContextM = makeContextHtmlM
+
+makeContextHtml :: (VarName -> GVal (Run (Writer Html) Html))
+                -> GingerContext (Writer Html) Html
+makeContextHtml l = makeContext' l toHtml
+
+makeContextHtmlM :: (Monad m, Functor m)
+                 => (VarName -> Run m Html (GVal (Run m Html)))
+                 -> (Html -> m ())
+                 -> GingerContext m Html
+makeContextHtmlM l w = makeContextM' l w toHtml
+
+makeContextText :: (VarName -> GVal (Run (Writer Text) Text))
+                -> GingerContext (Writer Text) Text
+makeContextText l = makeContext' l asText
+
+makeContextTextM :: (Monad m, Functor m)
+                 => (VarName -> Run m Text (GVal (Run m Text)))
+                 -> (Text -> m ())
+                 -> GingerContext m Text
+makeContextTextM l w = makeContextM' l w asText
+
+
+data RunState m h
+    = RunState
+        { rsScope :: HashMap VarName (GVal (Run m h))
+        , rsCapture :: h
+        , rsCurrentTemplate :: Template -- the template we are currently running
+        , rsCurrentBlockName :: Maybe Text -- the name of the innermost block we're currently in
+        }
+
+-- | Internal type alias for our template-runner monad stack.
+type Run m h = StateT (RunState m h) (ReaderT (GingerContext m h) m)
+
+-- | Lift a value from the host monad @m@ into the 'Run' monad.
+liftRun :: Monad m => m a -> Run m h a
+liftRun = lift . lift
+
+-- | Lift a function from the host monad @m@ into the 'Run' monad.
+liftRun2 :: Monad m => (a -> m b) -> a -> Run m h b
+liftRun2 f x = liftRun $ f x
+
+
