Применение обобщённой свёртки для обработки синтаксических деревьев
Привет, Хабр! В рамках данной статьи мы создадим интерпретатор для простого языка программирования с использованием обобщённой свёртки. Далее следует небольшое введение. Для тех, кто уже знаком с типом 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
для преобразования всех значений в этом типе в одно. Рассмотрим плюсы данного подхода на примере интерпретации абстрактного синтаксического дерева (далее АСД):
Обработка синтаксического дерева без явного использования рекурсии позволяет избавиться от ошибок, связанных с тем, что мы забыли вызвать нашу функцию для одного из листов дерева. Например, когда мы вызвали функцию в операторе
If
для условия иthen
блока, а дляelse
забыли. Также это позволяет писать менее нагруженный код, так как явной рекурсии в коде больше не будет.Возможность снабдить каждый лист дерева дополнительной информацией и гарантировать её наличие. В нашем случае каждый лист дерева будет снабжён информацией о позиции внутри файла с программой, что позволит нам печатать сообщения об ошибках вместе со строкой, на которой она произошла.
Приступим к разработке интерпретатора.
Синтаксис
Поддерживаются только переменные с типом 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.