Применение обобщённой свёртки для обработки синтаксических деревьев

697b77be190954644552d81563a8a0d3.png

Привет, Хабр! В рамках данной статьи мы создадим интерпретатор для простого языка программирования с использованием обобщённой свёртки. Далее следует небольшое введение. Для тех, кто уже знаком с типом Fix — фиксированной точкой для функторов, оставляю ссылку на основную часть статьи.

Правая свёртка для списков

Свёртка — операция, которая разрушает исходную структуру типа и возвращает одно значение. Например,  преобразует список чисел в их сумму, при этом сама структура списка как бы разрушается. Рассмотрим правую свёртку для списка:

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f ini (x:xs) = f x (foldr f ini xs)
foldr _ ini _      = ini

Как можно заметить, функция f, которая передаётся первым аргументом, вызывается для каждого элемента списка, но при этом в ней не реализован рекурсивный обход списка. Получается, что в foldr реализуется обход списка и вызов нашей функции для каждого его элемента, нам же остаётся реализовать только непосредственно обработку. Возникает вопрос — можем ли мы написать функцию, аналогичную foldr,   которая будет работать для любого рекурсивного типа данных. Ответ — да, можем. Как, расскажу в следующих разделах.

Тип Fix

В данном разделе мы рассмотрим структуру данных список и выделим на его примере общий для всех рекурсивных типов паттерн.

data List a = Nil | Elem a (List a)

Давайте попробуем убрать рекурсию из данного типа, добавив параметр типа:

data List a b = Nil | Elem a b

Попробуем теперь создать список с двумя элементами:

xs = Elem 1 (Elem 2 Nil)

Прекрасно! У нас получилось — объявление типа списка теперь лишено рекурсии. Но есть одно, но — xs имеет тип не List Int, как это было прежде, а List Int (List Int (List Int a)). То есть теперь мы не можем создать функцию, которая работает со списком, имеющим произвольный размер. На помощь нам приходит тип с фиксированной точкой.

newtype Fix f = Fix { unFix :: f (Fix f) }

Раньше вместо параметра b в типе List мы подставляли List a, теперь если мы передадим в качестве параметра List a в Fix, конструктор Fix сделает то же самое за нас:

type FixedList a = Fix (List a)

Снова создадим список с двумя элементами:

xs :: FixedList Int
xs = Fix (Elem 1 (Fix (Elem 2 (Fix Nil))))

Можно радоваться — мы и рекурсию из типа убрали и тип теперь одинаков для любого количества элементов в списке. Аналогичные преобразования можно применить к любому рекурсивному типу, просто заменив его вхождения на параметр типа.

Пишем свёртку для Fix

Мы можем использовать второй параметр типа List, который ввели, чтобы исключить рекурсию из определения, для хранения значения аккумулятора. Рассмотрим пример склейки в строку всех значений списка с двумя элементами. Далее будет приведён код на псевдохаскеле, в каждом примере мы будем обрабатывать следующую часть списка:

xs :: FixedList Int
xs = Fix (Elem 1 (Fix (Elem 2 (Fix Nil))))

Первый шаг

Преобразуем Nil в начальное значение аккумулятора, то есть в пустую строку:

f :: List Int String -> String
f Nil = ""

Второй шаг

Преобразуем второй элемент списка и значение аккумулятора в новое значение аккумулятора:

f :: List Int String -> String
f (Elem 2 acc) = "2" ++ acc

Третий шаг

Преобразуем первый элемент списка и значение аккумулятора в новое значение аккумулятора:

f :: List Int String -> String
f (Elem 1 acc) = "1" + acc

В результате получаем »12». В данном примере f — функция обработки списка, которую мы будем передавать в нашу свёртку.

Когда идея того, как должна работать свёртка для типа Fix понятна, реализуем её.

foldFix :: Functor f => (f a -> a) -> Fix f -> a
foldFix f = f . fmap (foldFix f) . unFix

Тип должен быть функтором, чтобы мы могли проникать в его структуру и выполнять свёртку, начиная с конца. a в данном случае является типом аккумулятора. Значение аккумулятора всегда будет находится в тех местах, куда мы поместили параметр типа для избавления от рекурсии.

Пишем развёртку для Fix

При написании интерпретатора нам понадобится развёртка для типа Fix. Развёртка позволяет сгенерировать рекурсивный тип данных из одного начального:

unfoldFix :: Functor f => (a -> f a) -> a -> Fix f
unfoldFix f = Fix . fmap (unfoldFix f) . f

Полный пример со списком:

{-# LANGUAGE DeriveFunctor #-}

module Main (main) where

import Prelude hiding (showList)

newtype Fix f = Fix { unFix :: f (Fix f) }

foldFix :: Functor f => (f a -> a) -> Fix f -> a
foldFix f = f . fmap (foldFix f) . unFix

unfoldFix :: Functor f => (a -> f a) -> a -> Fix f
unfoldFix f = Fix . fmap (unfoldFix f) . f

type List a = Fix (ListF a)

data ListF a b = Nil | Elem a b deriving Functor

showList :: Show a => List a -> String
showList = foldFix showListF

showListF :: Show a => ListF a String -> String
showListF Nil          = ""
showListF (Elem x acc) = show x ++ acc

-- Список чисел от 0 до 3
list :: List Int
list = unfoldFix (\x -> if x <= 3 then Elem x (x + 1) else Nil) 0

-- Выведет в консоль 0123
main :: IO ()
main = putStrLn $ showList list

Стоит отметить, что в дальнейшем мы не будем использовать самописный тип Fix, так как он уже реализован в пакете data-fix.

Создаём интерпретатор

Итак, мы можем избавить определение любого рекурсивного типа данных от рекурсии и использовать функцию foldFix для преобразования всех значений в этом типе в одно. Рассмотрим плюсы данного подхода на примере интерпретации абстрактного синтаксического дерева (далее АСД):

  1. Обработка синтаксического дерева без явного использования рекурсии позволяет избавиться от ошибок, связанных с тем, что мы забыли вызвать нашу функцию для одного из листов дерева. Например,   когда мы вызвали функцию в операторе If для условия и then блока, а для else забыли. Также это позволяет писать менее нагруженный код, так как явной рекурсии в коде больше не будет.

  2. Возможность снабдить каждый лист дерева дополнительной информацией и гарантировать её наличие.  В нашем случае каждый лист дерева будет снабжён информацией о позиции внутри файла с программой, что позволит нам печатать сообщения об ошибках вместе со строкой, на которой она произошла.

Приступим к разработке интерпретатора.

Синтаксис

Поддерживаются только переменные с типом int и bool, условный оператор if, цикл while и рекурсивные функции. Пример программы приведён ниже:

def fac_rec(x) {
    if x == 2 {
        return x
    } else {
        return x * fac_rec(x - 1)
    }
}

def fac_iter(x) {
    res = 1
    while x >= 2 {
        res = x * res
        x = x - 1
    }
    return res
}

def main() {
    while true {
        print("Enter mode:\n1) recursive\n2) iterative\n3) exit\n")
        mode = read()
        if mode == 1 || mode == 2 {
            print("Enter x: ")
            x = read()
            res = 0
            if mode == 1 {
                res = fac_rec(x)
            } else {
                res = fac_iter(x)
            }
            print("x!: ", res, "\n")
        } else {
            if mode == 3 {
                break
            } else {
                print("mode is invalid\n")
            }
        }
    }
}

АСД

Напишем рекурсивный тип для представления АСД:

data Function = Function
    { funName :: Text
    , funArgs :: [Text]
    , funBody :: Expr
    } deriving Eq

data Expr
    = IntLit Int
    | BoolLit Bool
    | Var Text
    | UnOp UnOp Expr
    | BinOp BinOp Expr Expr
    | Assign Text Expr
    | If Expr Expr (Maybe Expr)
    | While Expr Expr
    | Seq [Expr]
    | Call Text [Expr]
    | Break
    | Read
    | Print [PrintArg]
    | Return (Maybe Expr)
    deriving Eq

data UnOp = Neg | Not deriving Eq

data BinOp
    = Add | Sub | Mul | Div
    | Lt | Le | Gt | Ge
    | Eq | Ne | And | Or
    deriving Eq

data PrintArg
    = StrArg Text
    | ExprArg Expr
    deriving Eq

Затем уберём рекурсию из определения типов:

module Lang.Ast.Types where

data Function b = Function
    { funName :: Text
    , funArgs :: [Text]
    , funBody :: b
    } deriving (Eq, Functor, Foldable, Traversable)

type Expr = Fix ExprF

data ExprF a
    = IntLit Int
    | BoolLit Bool
    | Var Text
    | UnOp UnOp a
    | BinOp BinOp a a
    | Assign Text a
    | If a a (Maybe a)
    | While a a
    | Seq [a]
    | Call Text [a]
    | Break
    | Read
    | Print [PrintArg a]
    | Return (Maybe a)
    deriving (Functor, Foldable, Traversable)

data UnOp = Neg | Not deriving Eq

data BinOp
    = Add | Sub | Mul | Div
    | Lt | Le | Gt | Ge
    | Eq | Ne | And | Or
    deriving Eq

data PrintArg a
    = StrArg Text
    | ExprArg a
    deriving (Functor, Foldable, Traversable)

Снабжаем каждый лист АСД позицией в файле

module Lang.Ast.Annotated where

-- Выражение, снабжённое позицией
type PosExpr = Fix PosExprF

type PosExprF = AnnF SourceSpan ExprF

-- Композиция функтора f с функтором аннотации
type AnnF ann f = Compose (Ann ann) f

-- Функтор, который содержит объект и его аннотацию
data Ann ann a = Ann
    { annotation :: ann
    , annotated  :: a
    } deriving (Functor, Foldable, Traversable)

-- Начало и конец выражения в файле
data SourceSpan = SourceSpan
    { spanBegin :: SourcePos
    , spanEnd   :: SourcePos
    } deriving (Eq, Show)

instance Semigroup SourceSpan where
	  s1 <> s2 = SourceSpan ((min on spanBegin) s1 s2) ((max on spanEnd) s1 s2)

-- Позиция в файле
data SourcePos = SourcePos
    { posFile   :: FilePath
    , posLine   :: Int
    , posColumn :: Int
    } deriving (Eq, Show)

instance Ord SourcePos where
    compare (SourcePos _ b1 e1) (SourcePos _ b2 e2) =  compare b1 b2
                                                    <> compare e1 e2

-- Функция для удаления всех аннотаций из синтаксического дерева
stripAnnotation :: Functor f => Fix (AnnF ann f) -> Fix f
stripAnnotation = unfoldFix (annotated . getCompose . unFix)

Инстанс Eq

Нам может понадобится сравнивать выражения. Для типа Fix f определён инстанс класса Eq, если для f определён инстанс класса Eq1. Напишем определения Eq1 для типов ExprF, PrintArg и Ann.

import Data.Functor.Classes (Eq1(..))

instance Eq1 ExprF where
    liftEq f a b = case (a, b) of
    (IntLit i1, IntLit i2)                 -> i1 == i2
    (BoolLit b1, BoolLit b2)               -> b1 == b2
    (Var v1, Var v2)                       -> v1 == v2
    (UnOp op1 e1, UnOp op2 e2)             -> op1 == op2
                                           && f e1 e2
    (BinOp op1 e11 e12, BinOp op2 e21 e22) -> op1 == op2
                                           && f e11 e21
                                           && f e12 e22
    (Assign v1 e1, Assign v2 e2)           -> v1 == v2 && f e1 e2
    (If cnd1 th1 el1, If cnd2 th2 el2)     -> f cnd1 cnd2
                                           && f th1 th2
    && liftEq f el1 el2
    (While cnd1 body1, While cnd2 body2)   -> f cnd1 cnd2 && f body1 body2
    (Seq ss1, Seq ss2)                     -> liftEq f ss1 ss2
    (Call fn1 args1, Call fn2 args2)       -> fn1 == fn2
                                           && liftEq f args1 args2
    (Break, Break)                         -> True
    (Read, Read)                           -> True
    (Print args1, Print args2)             -> liftEq (liftEq f) args1 args2
    (Return v1, Return v2)                 -> liftEq f v1 v2
    _                                      -> False

instance Eq1 PrintArg where
    liftEq f a b = case (a, b) of
    (StrArg s1, StrArg s2)   -> s1 == s2
    (ExprArg e1, ExprArg e2) -> f e1 e2
    _                        -> False

instance Eq ann => Eq1 (Ann ann) where
    liftEq f (Ann a1 g1) (Ann a2 g2) = a1 == a2 && f g1 g2

Парсер

Про аппликативные парсеры есть куча хороших статей, поэтому я не буду останавливаться на полной реализации парсера, её можно посмотреть в репозитории. Здесь для примера того, как снабжать выражения позицией в файле, приведу код парсера для целочисленных констант:

import Data.Fix               (Fix(..))
import Data.Functor.Compose   (Compose(..))
import Data.Text              (Text)
import Data.Void              (Void)
import Text.Megaparsec hiding (SourcePos(..))

import Lang.Ast.Annotated
import Lang.Ast.Types

import qualified Text.Megaparsec as Megaparsec
import qualified Text.Megaparsec.Char.Lexer as Lexer

type Parser = Parsec Void Text

pIntLit :: Parser PosExpr
pIntLit = annotate (IntLit <$> Lexer.decimal)

annotate :: Parser (f (Fix (AnnF SourceSpan f)))
         -> Parser (Fix (AnnF SourceSpan f))
annotate p = do
    from <- convertPos <> getSourcePos
    let ann = SourceSpan from to
    pure $ Fix (Compose $ Ann ann res)
    where convertPos :: Megaparsec.SourcePos -> SourcePos
          convertPos (Megaparsec.SourcePos path from to) =
        	    SourcePos path (Megaparsec.unPos from) (Megaparsec.unPos to)

Интерпретатор

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

-- В эту монаду будут интерпретироваться выражения, снабжённые позицией
newtype PosInterpreterM a = PosInterpreterM
    { runPosInterpreterM :: ReaderT (Context PosInterpreterM) IO a
    } deriving ( Functor
               , Applicative
               , Monad
               , MonadReader (Context PosInterpreterM)
               , MonadIO
               )

-- В эту монаду будут интерпретироваться выражения, не снабжённые позицией
newtype InterpreterM a = InterpreterM
    { runInterpreterM :: ReaderT (Context InterpreterM) IO a
    } deriving ( Functor
               , Applicative
               , Monad
               , MonadReader (Context InterpreterM)
               , MonadIO
               )

data Context m = Context
    { ctxVariables   :: IORef (HashMap Text Value) -- Значения переменных в текущей функции
    , ctxFunctions   :: HashMap Text (FuncInfo m)  -- Функции
    , ctxSourceSpan  :: SourceSpan                 -- Позиция в файле для текущего выражения
    , ctxSourceCode  :: Text                       -- Код программы
    , ctxIsBreak     :: IORef Bool                 -- Нужно ли выйти из цикла
    , ctxIsReturn    :: IORef Bool                 -- Нужно ли выйти из функции
    , ctxReturnValue :: IORef (Maybe Value)        -- Значение, которое вернула текущая функция
    }

data FuncInfo m = FuncInfo
    { infArgs :: ![Text]            -- Список аргументов функции
    , infBody :: !(m (Maybe Value)) -- Тело функции
    }

data Value
    = IntValue Int
    | BoolValue Bool
    deriving Eq

Нам также понадобится класс типов для полиморфной функции возврата ошибки:

class Monad m => MonadError m where
    throwError :: Text -> m a

Напишем инстансы для наших монад:

instance MonadError PosInterpreterM where
    throwError err = do
        SourcePos fpath ln cl <- asks (spanBegin . ctxSourceSpan)
        src <- asks ctxSourceCode
        let strLn = Text.pack $ show ln
            margin = Text.replicate (Text.length strLn + 2) " "
            errMsg =  Text.pack fpath <> ":" <> strLn <> ":"
                                      <> Text.pack (show cl)
                                      <> ": error:\n" <> err <> "\n"
            line = Text.takeWhile (/='\n')
                 . (!!(ln-1))
                 . iterate (Text.tail . Text.dropWhile (/= '\n')) $ src
            prettyLine =  margin <> "|\n " <> strLn <> " | " <> line <> "\n"
                       <> margin <> "|\n"
        liftIO $ throwIO $ InterpreterException (errMsg <> prettyLine)

instance MonadError InterpreterM where
		throwError = liftIO . throwIO . InterpreterException

data InterpreterException = InterpreterException Text

instance Show InterpreterException where
		show (InterpreterException err) =  "InterpreterException: "
                                    ++ Text.unpack err

instance Exception InterpreterException where

Для PosInterpreterM ошибка будет выглядеть следующим образом:

InterpreterException: example.lang:19:12:
   |
19 |     while 1 {
   |
1 is not a boolean value

А для InterpreterM следующим:

InterpreterException: 1 is not a boolean value

Далее напишем интерпретатор для выражений без позиции. Каждое выражение возвращает либо Int, либо Bool, либо ничего, поэтому тип возвращаемого значения m (Maybe Value):

interpretExpr :: (MonadReader (Context m) m, MonadError m, MonadInput m)
              => Expr
              -> m (Maybe Value)
interpretExpr = foldFix interpretExprF

-- Пока оставим заглушку
interpretExprF :: (MonadReader (Context m) m, MonadError m, MonadInput m)
               => ExprF (m (Maybe Value))
               -> m (Maybe Value)
interpretExprF = undefined

Самое интересное начинается сейчас — мы напишем интерпретатор для выражений с позицией, используя ту же функцию interpretExprF, что и для выражений без позиции. При этом перед вызовом interpretExprF для каждого листа синтаксического дерева, мы будем класть в контекст позицию этого листа в коде и затем убирать её из листа. Если бы мы использовали для представления синтаксического дерева обычный рекурсивный тип данных, нам пришлось бы явно класть в каждый лист позицию в коде:

-- Обработка дерева, явно содержащего позицию
interpretExpr (IntLit x pos) = ...

-- Обработка дерева, неявно содержащего позицию
interpretExprF (IntLit x) = ...

Для этого нам потребуется вспомогательная функция:

adi :: Functor f => (f a -> a) -> ((Fix f -> a) -> Fix f -> a) -> Fix f -> a
adi f g = g (f . fmap (adi f g) . unFix)

Я позаимствовал её из проекта hnix. Основная идея заключается в том, что перед вычислением свёртки мы вызываем функцию g, которая позволяет сделать что-нибудь с текущим листом, например, модифицировать или извлечь информацию. В нашем случае мы будем доставать из листа позицию, класть её в контекст и после вызывать вычисление свёртки. Делать мы это будем в функции setContext:

setContext :: (PosExpr -> InterpeterM (Maybe Value))
           -> PosExpr
           -> InterpeterM (Maybe Value)
setContext f expr = local (\ctx -> ctx { ctxSourceSpan = sourceSpan }) $
    f expr
  where sourceSpan = annotation $ getCompose $ unFix expr

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

interpretPosExpr :: PosExpr -> PosInterpreterM (Maybe Value)
interpretPosExpr = adi (interpretExprF . annotated . getCompose) setContext

Теперь реализуем функцию interpretExprF. Сначала нужно будет реализовать функцию shouldSkip,  которая будет вычислять выражение только в том случае, если переменные контекста ctxIsBreak и ctxIsReturn не установлены. Это нужно, чтобы инструкции после Break и Return не исполнялись:

shouldSkip :: (MonadReader (Context m) m, MonadIO m)
           => m (Maybe a)
           -> m (Maybe a)
shouldSkip ma = do
    brk <- asks ctxIsBreak >>= liftIO . readIORef
    ret <- asks ctxIsReturn >>= liftIO . readIORef
    if brk || ret then pure Nothing else ma

Написание интерпретатора начнём с числовых и булевых выражений. Мы просто преобразуем их в тип Value с помощью конструкторов IntValue и BoolValue соответственно:

interpretExprF :: (MonadReader (Context m) m, MonadError m, MonadIO m)
               => ExprF (m (Maybe Value))
               -> m (Maybe Value)
interpretExprF = shouldSkip . \case
    IntLit i  -> pure $ Just $ IntValue i
    BoolLit b -> pure $ Just $ BoolValue b

Далее переходим к переменным. Если удалось найти значение по имени переменной в хэш-таблице ctxVariables, то возвращаем его, иначе кидаем исключение:

    Var x ->
        variablesRef <- asks ctxVariables
        mValue <- HashMap.lookup v <$> liftIO (readIORef variablesRef)
        case mValue of
            Nothing    -> throwError $ "undefined variable " <> v
            Just value -> pure $ Just value

Далее будем интерпретировать унарные и бинарные операции. Для этого нам понадобятся три вспомогательные функции:

-- Функция возвращает значение типа Int, полученное из результата вычисления,
-- или ошибку
intValue :: MonadError m => m (Maybe Value) -> m Int
intValue mVal = mVal >>= \case
    Just (IntValue val) -> pure val
    Just BoolValue{}    -> typeError "int" "bool"
    Nothing             -> typeError "int" "unit"

-- Функция возвращает значение типа Bool, полученное из результата вычисления,
-- или ошибку
boolValue :: MonadError m => m (Maybe Value) -> m Bool
boolValue mVal = mVal >>= \case
    Just (BoolValue val) -> pure val
    Just IntValue{}      -> typeError "bool" "int"
    Nothing              -> typeError "bool" "unit"

-- Функция возвращает ошибку типизации
typeError :: MonadError m => Text -> Text -> m a
typeError expected actual = throwError $  "type error: expected "
                                       <> expected
                                       <> " actual "
                                       <> actual

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

    UnOp op mVal -> case op of
        Neg -> Just . IntValue . negate <$> intValue mVal
        Not -> Just . BoolValue . not <$> boolValue mVal
    BinOp op mVal1 mVal2 -> case op of
        Add -> Just . IntValue <$> ((+) <$> intValue mVal1 <*> intValue mVal2)
        Eq  -> Just . BoolValue <$> ((==) <$> mVal1 <*> mVal2)
        And -> do
            val1 <- boolValue mVal1
            if val1
               then Just . BoolValue <$> boolValue mVal2
               else pure $ Just $ BoolValue False

Далее по списку идёт операция присваивания. Для неё нужно написать ещё одну вспомогательную функцию:

anyValue :: MonadError m => m (Maybe a) -> m a
anyValue mVal = mVal >>= maybe (typeError "int or bool" "unit") pure

Код для самого присваивания приведён ниже:

    Assign var mVal -> do
        val <- anyValue mVal
        variablesRef <- asks ctxVariables
        liftIO $ modifyIORef variablesRef (HashMap.insert var val)
        pure Nothing

Теперь напишем реализацию условных выражений:

    If mCnd th el -> do
        cnd <- boolValue mCnd
        if cnd then th else fromMaybe (pure Nothing) el

Затем реализуем операции Break и Return:

   -- Устанавливаем ctxIsBreak
    Break -> do
        brkRef <- asks ctxIsBreak
        liftIO $ writeIORef brkRef True
        pure Nothing

    -- Устанавливаем ctxIsReturn, вычисляем возвращаемое значение
    -- и кладём его в ctxReturnValue
    Return mVal -> do
        val <- fromMaybe (pure Nothing) mVal
        returnValue <- asks ctxReturnValue
        isReturn <- asks ctxIsReturn
        liftIO $ do
            writeIORef returnValue val
            writeIORef isReturn True
            pure Nothing

Для цикла while нам понадобятся следующие функции:

-- Выполняет тело цикла, пока условие не вернёт False
while :: Monad m => m Bool -> m a -> m ()
while mCond mbody = do
    cond <- mCond
    when cond (mbody >> while mCond mbody)

-- Если в цикле была исполнена одна из операций break или return,
-- то будет установлена одна из переменных ctxIsBreak или ctxIsReturn.
-- Поэтому перед вычислением условия, мы проверяем эти переменные.
-- Если одна из них установлена, возвращаем False, иначе возвращаем
-- результат условия. Перед выходом из цикла, сбрасываем ctxIsBreak.
whileCond :: (MonadReader (Context m) m, MonadError m, MonadIO m)
          => m (Maybe Value)
          -> m Bool
whileCond mCond = do
    brkRef <- asks ctxIsBreak
    brk <- liftIO $ readIORef brkRef
    ret <- asks ctxIsReturn >>= liftIO . readIORef
    if brk || ret
       then liftIO (writeIORef brkRef False) >> pure False
       else boolValue mCond

Реализация цикла while выглядит следующим образом:

    While mCond mBody -> while (whileCond mCond) mBody >> pure Nothing

Далее реализуем операцию Seq — последовательное исполнение команд.

    Seq ss -> sequence_ ss >> pure Nothing

Следующий шаг — вызов функции. Напишем очередную вспомогательную функцию:

callFunction :: (MonadReader (Context m) m, MonadError m, MonadIO m)
             => Text
             -> [Value]
             -> m (Maybe Value)
callFunction fun args = do
    -- Находим тело функции по имени, если не находим, кидаем исключение.
    funcs <- asks ctxFunctions
    case HashMap.lookup fun funcs of
        Nothing    -> throwError $ "undefined function " <> fun
        Just fInfo -> call fInfo
  where call (FuncInfo argNames body)
          -- Если количество переданных аргументов не равно количеству
          -- ожидаемых, кидаем исключение
          | actualNumArgs /= expectedNumArgs
          = throwError $  "function " <> fun <> " expected "
                       <> Text.pack (show expectedNumArgs)
                       <> " arguments but given "
                       <> Text.pack (show actualNumArgs)
          | otherwise = do
              -- Запоминаем значения переменных для текущей функции
              -- и пишем в ctxVariables значения аргументов для
              -- вызываемой функции
              let argsMap = HashMap.fromList $ zip argNames args
              variablesRef <- asks ctxVariables
              backupVars <- liftIO $ readIORef variablesRef
              liftIO $ writeIORef variablesRef argsMap
          
              -- Вызываем тело функции
              void body

              -- Восстанаваливаем значения переменных текущей функции.
              -- Сбрасываем ctxIsReturn, достаём возвращаемой значение
              -- из ctxReturnValue и возвращаем его
              liftIO $ writeIORef variablesRef backupVars
              returnValueRef <- asks ctxReturnValue
              isReturnRef <- asks ctxIsReturn
              returnValue <- liftIO $ readIORef returnValueRef
              liftIO $ do
                  writeIORef returnValueRef Nothing
                  writeIORef isReturnRef False
                  pure returnValue
           where actualNumArgs = length args
                 expectedNumArgs = length argNames

Реализации вызова функции приведена ниже:

    Call fun mArgs -> do
        args <- anyValue (sequence <$> sequence mArgs)
        callFunction fun args

Остаётся реализовать только ввод-вывод:

    Read -> do
        str <- liftIO Text.getLine
        case Text.signed Text.decimal str of
            Right (res, _) -> pure $ Just $ IntValue res
            Left{}
              | str == "true"  -> pure $ Just $ BoolValue True
              | str == "false" -> pure $ Just $ BoolValue False
              | otherwise      -> liftIO $ throwIO $
                  InterpreterException "failed to read value"
    Print args -> do
        forM_ args $ \case
            StrArg str   -> liftIO $ Text.putStr str
            ExprArg mVal -> mVal >>= \case
                Just (IntValue i)  -> liftIO $ putStr $ show i
                Just (BoolValue b) -> liftIO $ Text.putStr $ if b
                                                                then "true"
                                                                else "false"
                Nothing            -> typeError "int or bool" "unit"
        pure Nothing

Интерпретатор для выражений мы написали, однако программа состоит из функций, поэтому нужно написать интерпретатор для списка функций. Таких интерпретаторов будет два — один для функций, выражения в которых снабжены позицией, второй — для тех, в которых не снабжены:

interpretFunctions :: Text -> [Function Expr] -> IO ()
interpretFunctions = genericInterpretFunctions interpretExpr toIO
  where toIO ctx ma = runReaderT (runInterpreterM ma) ctx

interpretPosFunctions :: Text -> [Function PosExpr] -> IO ()
interpretPosFunctions = genericInterpretFunctions interpretPosExpr toIO
  where toIO ctx ma = runReaderT (runPosInterpreterM ma) ctx

genericInterpretFunctions :: (MonadReader (Context m) m, MonadError m, MonadIO m)
                          => (e -> m (Maybe Value))
                          -> (Context m -> m (Maybe Value) -> IO (Maybe Value))
                          -> Text
                          -> [Function e]
                          -> IO ()
genericInterpretFunctions eval toIO sourceCode funDefs = do
    variables <- newIORef HashMap.empty
    isBreak <- newIORef False
    isReturn <- newIORef False
    retValue <- newIORef Nothing
    let ctx = Context
            { ctxVariables   = variables
            , ctxFunctions   = functions
            , ctxSourceSpan  = SourceSpan initPos initPos
            , ctxSourceCode  = sourceCode
            , ctxIsBreak     = isBreak
            , ctxIsReturn    = isReturn
            , ctxReturnValue = retValue
            }
    void $ toIO ctx (callFunction "main" [])
  where initPos = SourcePos "" 1 1
        functions = foldl collectFunction HashMap.empty funDefs
        collectFunction funcs (Function name args body) =
            HashMap.insert name func funcs
          where func = FuncInfo args (eval body)

Собираем всё вместе

Все части интерпретатора написаны, осталось соединить их вместе:

module Main (main) where

import System.Environment (getArgs, getProgName)
import System.Exit        (die)

import Lang.Ast    (interpretPosFunctions)
import Lang.Parser (parseFromText)

import qualified Data.Text.IO as Text

main :: IO ()
main = do
    fileName <- getFileName
    sourceCode <- Text.readFile fileName
    case parseFromText fileName sourceCode of
        Left err -> Text.putStrLn err
        Right fs -> interpretPosFunctions sourceCode fs
  where getFileName = getArgs >>= \case
            [x] -> pure x
            _   -> do
                progName <- getProgName
                die $ "Usage: " ++ progName ++ " "

Итоговый код интерпретатора можно посмотреть в репозитории.

Заключение

Интерпретаторы императивных языков не самым лучшим образом реализуются посредством свёртки. Для корректной работы операций break и return пришлось применять хак с установкой флагов ctxIsBreak и ctxIsReturn и их проверкой перед исполнением каждой операции. Для раскрытия темы я выбрал интерпретатор императивного, как более наглядный и простой пример. В качестве более красивого применения описанного подхода, могу привести проверку типов, реализованную мной для двух языков: harakiri и tiger, трансляцию синтаксического дерева языка harakiri в промежуточное представление и проект hnix, в котором реализуется интерпретатор для функционального языка программирования nix.

© Habrahabr.ru