Logging
Logging allows the compiler to keep track of arbitrary information as it runs. This information can be helpful for debugging, when things go wrong.
1 Redesign the interface
As with the configuration, each phase needs access to the log. Unlike configuration, each phase may also modify the log!. That means that the output of each phase should include the new log value. To do so, we will change the signature of each phase so that it returns a tuple. The tuple contains both the result of the phase and the log as it should appear after the phase has run.
The new signatures for our compiler are:
compiler :: String -> CompilerConfiguration -> CompilerLog -> (String, CompilerLog)
tokenize :: String -> CompilerConfiguration -> CompilerLog -> ([Token], CompilerLog)
parse :: [Token] -> CompilerConfiguration -> CompilerLog -> (AST, CompilerLog)
optimize :: AST -> CompilerConfiguration -> CompilerLog -> (AST, CompilerLog)
emit :: AST -> CompilerConfiguration -> CompilerLog -> (String, CompilerLog)2 Composing phases
To compose phases with logging, we need to thread the output result and the output log of one phase to the corresponding inputs of the next. And, of course, we need to feed the configuration to each phase.
Let us update the definition of our >.> phase-composition operator.
-- | Compose two compiler phases into a single phase
(>.>) :: (a -> CompilerConfiguration -> CompilerLog -> (b, CompilerLog)) -> (b -> CompilerConfiguration -> CompilerLog -> (c, CompilerLog)) -> (a -> CompilerConfiguration -> CompilerLog -> (c, CompilerLog))
(>.>) phase1 phase2 input configuration log =
let (phase1Result, log') = phase1 input configuration log
(phase2Result, log'') = phase2 phase1Result configuration log'
in (phase2Result, log'')Not too bad! Yes, the type of (>.>) is getting completely out of control. But other than that, we are still able to “contain” the complexity of our design within the implementation of our composition operator.
3 Full code
Here is all the code for our compiler, whose phases can read a configuration and read/write a log.
The code that has changed from our previous version is emphasized.
Compiler.hs
module Compiler where
{- Compiler pipeline -}
compiler :: String -> CompilerConfiguration -> CompilerLog -> (String, CompilerLog)
compiler = tokenize >.> parse >.> optimize >.> emit
-- | Compose two compiler phases into a single phase
(>.>) :: (a -> CompilerConfiguration -> CompilerLog -> (b, CompilerLog)) -> (b -> CompilerConfiguration -> CompilerLog -> (c, CompilerLog)) -> (a -> CompilerConfiguration -> CompilerLog -> (c, CompilerLog))
(>.>) phase1 phase2 input configuration log =
let (phase1Result, log') = phase1 input configuration log
(phase2Result, log'') = phase2 phase1Result configuration log'
in (phase2Result, log'')
{- Compiler phases -}
tokenize :: String -> CompilerConfiguration -> CompilerLog -> ([Token], CompilerLog)
tokenize = undefined
parse :: [Token] -> CompilerConfiguration -> CompilerLog -> (AST, CompilerLog)
parse = undefined
optimize :: AST -> CompilerConfiguration -> CompilerLog -> (AST, CompilerLog)
optimize = undefined
emit :: AST -> CompilerConfiguration -> CompilerLog -> (String, CompilerLog)
emit = undefined
{- Compiler data types -}
data Token = Token
data AST = AST
data CompilerConfiguration = CompilerConfiguration
data CompilerLog = CompilerLog
data CompilerState = CompilerState
data CompilerError = CompilerError