Генерация кода для компилятора в Haskell - PullRequest
5 голосов
/ 10 июня 2011

Я пишу компилятор для небольшого императивного языка.Целевым языком является байт-код Java, а компилятор реализован на языке Haskell.

Я написал внешний интерфейс для языка - то есть у меня есть лексер, анализатор и средство проверки типов.У меня проблемы с выяснением, как сделать генерацию кода.

Я сохраняю структуру данных, представляющую стек локальных переменных.Я могу запросить эту структуру с именем локальной переменной и получить ее позицию в стеке.Эта структура данных передается при обходе дерева синтаксиса, а переменные выталкиваются и выдвигаются при входе и выходе из новых областей.

Что мне не удается понять, так это как генерировать байт-код.Выдача строк на терминалах и объединение их на более высоких уровнях кажется плохим решением, как с точки зрения ясности, так и с точки зрения производительности.

tl; dr Как я могу генерировать байт-код при выполнении дерева синтаксиса?

Ответы [ 2 ]

4 голосов
/ 11 июня 2011

Мой первый проект на Haskell несколько месяцев назад состоял в написании компилятора c, и в результате я получил довольно наивный подход к генерации кода, который я расскажу здесь. Пожалуйста, , а не , возьмите это как пример хорошего дизайна для генератора кода, а скорее рассмотрите его как быстрый и грязный (и в конечном счете наивный) способ получить что-то, что работает довольно быстро с приличной производительностью.

Я начал с определения промежуточного представления 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)

Затем появилась монада, которая будет обрабатывать состояние чередования на протяжении всего перевода (я блаженно не подозревала о нашем друге - 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)
    }

Для удобства я также указал ряд монадических функций переводчика, например:

-- |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 за раз.

2 голосов
/ 10 июня 2011

Если вы еще не сделали этого раньше, вы можете сделать это небольшими проходами: 1) для каждого оператора создайте некоторый байт-код (без правильно адресованных областей памяти) 2) после того, как это будет сделано, если у вас есть цикл, gotosи т. д., введите реальные адреса (теперь вы знаете их, когда все у вас есть) 3) замените выборки / хранилища памяти на правильные места 4) выгрузите их в файл JAR

Обратите внимание, чтоэто очень упрощено и не пытается оптимизировать производительность.Это даст вам функциональную программу, которая будет выполняться.Это также предполагает, что вы знаете коды для JVM (именно здесь я предполагаю, что вы собираетесь ее выполнить).

Для начала, просто есть подмножество языка, который делает последовательные арифметические операторы.Это позволит вам выяснить, как отобразить переменные ячейки памяти в операторы через дерево разбора.Затем добавьте цикл, чтобы заставить прыжки работать.Аналогичным образом добавьте условные выражения.Наконец, вы можете добавить заключительные части вашего языка.

...