我正在编写一个小型命令式语言的编译器。目标语言是Java字节码,编译器在Haskell中实现。
我已经为该语言编写了一个前端 - 即我有一个词法分析器,解析器和类型检查器。我无法弄清楚如何进行代码生成。
我保留了一个表示局部变量堆栈的数据结构。我可以使用局部变量的名称查询此结构并获取其在堆栈中的位置。当我遍历语法树时,这个数据结构会被传递,当我进入和退出新范围时,会弹出和推送变量。
我弄清楚的是如何发出字节码。在终端发出字符串并将它们连接在较高级别似乎是一个糟糕的解决方案,无论是清晰度还是性能方面。
tl; dr 如何在使用语法树时发出字节码?
答案 0 :(得分:4)
几个月前我在Haskell的第一个项目是编写一个c编译器,结果是一种相当天真的代码生成方法,我将在这里介绍。请不将此作为代码生成器的良好设计示例,而是将其视为一种快速而肮脏(并且最终天真)的方式,以获得具有良好性能的相当快速工作的方式。 / p>
我首先定义了一个中间表示LIR(低级中间表示),它与我的指令集(在我的情况下为x86_64)密切对应:
data LIRInst = LIRRegAssignInst LIRReg LIRExpr
| LIRRegOffAssignInst LIRReg LIRReg LIRSize LIROperand
| LIRStoreInst LIRMemAddr LIROperand
| LIRLoadInst LIRReg LIRMemAddr
| LIREnterInst LIRInt
| LIRJumpLabelInst LIRLabel
| LIRIfInst LIRRelExpr LIRLabel LIRLabel -- false, then true
| LIRCallInst LIRLabel LIRLabel -- method label, return label
| LIRCalloutInst String
| LIRRetInst [LIRLabel] String -- list of successors, and the name of the method returning from
| LIRLabelInst LIRLabel
deriving (Show, Eq, Typeable)
接下来是一个monad,它将在整个翻译过程中处理交错状态(我很幸福地没有意识到我们的朋友 - State Monad
- 当时):
newtype LIRTranslator a = LIRTranslator
{ runLIR :: Namespace -> (a, Namespace) }
instance Monad LIRTranslator where
return a = LIRTranslator (\s -> (a, s))
m >>= f = LIRTranslator (\s ->
let (a, s') = runLIR m s
in runLIR (f a) s')
以及将在各个翻译阶段“线程化”的状态:
data Namespace = Namespace
{ temp :: Int -- id's for new temporaries
, labels :: Int -- id's for new labels
, scope :: [(LIRLabel, LIRLabel)] -- current program scope
, encMethod :: String -- current enclosing method
, blockindex :: [Int] -- index into the SymbolTree
, successorMap :: Map.Map String [LIRLabel]
, ivarStack :: [(LIRReg, [CFGInst])] -- stack of ivars (see motioned code)
}
为方便起见,我还指定了一系列翻译monadic函数,例如:
-- |Increment our translator's label counter
incLabel :: LIRTranslator Int
incLabel = LIRTranslator (\ns@(Namespace{ labels = l }) -> (l, ns{ labels = (l+1) }))
然后我逐步递归模式匹配我的AST,逐个片段,导致形式的很多函数:
translateBlock :: SymbolTree -> ASTBlock -> LIRTranslator [LIRInst]
translateBlock st (DecafBlock _ [] _) = withBlock (return [])
translateBlock st block =
withBlock (do b <- getBlock
let st' = select b st
declarations <- mapM (translateVarDeclaration st') (blockVars block)
statements <- mapM (translateStm st') (blockStms block)
return (concat declarations ++ concat statements))
(用于翻译目标语言代码的块)或
-- | Given a SymbolTree, Translate a single DecafMethodStm into [LIRInst]
translateStm st (DecafMethodStm mc _) =
do (instructions, operand) <- translateMethodCall st mc
final <- motionCode instructions
return final
(用于翻译方法调用)或
translateMethodPrologue :: SymbolTree -> DecafMethod -> LIRTranslator [LIRInst]
translateMethodPrologue st (DecafMethod _ ident args _ _) =
do let numRegVars = min (length args) 6
regvars = map genRegVar (zip [LRDI, LRSI, LRDX, LRCX, LR8, LR9] args)
stackvars <- mapM genStackVar (zip [1..] (drop numRegVars args))
return (regvars ++ stackvars)
where
genRegVar (reg, arg) =
LIRRegAssignInst (symVar arg st) (LIROperExpr $ LIRRegOperand reg)
genStackVar (index, arg) =
do let mem = LIRMemAddr LRBP Nothing ((index + 1) * 8) qword -- ^ [rbp] = old rbp; [rbp + 8] = ret address; [rbp + 16] = first stack param
return $ LIRLoadInst (symVar arg st) mem
以实际生成一些LIR代码为例。希望这三个例子能给你一个很好的起点;最终,你会想要慢慢地,一次关注AST中的一个片段(或中间类型)。
答案 1 :(得分:2)
如果您之前没有这样做,可以通过小通行证: 1)为每个语句生成一些字节代码(没有正确寻址的内存位置) 2)完成后,如果你有循环,gotos等,放入真实的地址(你知道它们 现在你已经全部铺好了) 3)用正确的位置替换内存提取/存储 4)将其转储到JAR文件
请注意,这是非常简化的,不会尝试进行任何性能优化。它将为您提供一个将执行的功能程序。这也假设您知道JVM的代码(我假设您将执行它。)
首先,只需要一个语言的子集来执行顺序算术语句。这将允许您弄清楚如何通过解析树将变量内存位置映射到语句。接下来添加一些循环以使跳转起作用。同样添加条件。最后,您可以添加语言的最后部分。