考虑一个简单的Haskell Brainf*ck解释器。看看interpret
函数就知道了。
import Prelude hiding (Either(..))
import Control.Monad
import Data.Char (ord, chr)
-- function in question
interpret :: String -> IO ()
interpret strprog = let (prog, []) = parse strprog
in execBF prog
interpretFile :: FilePath -> IO ()
interpretFile fp = readFile fp >>= interpret
type BF = [BFInstr]
data BFInstr = Left | Right | Inc | Dec | Input | Output | Loop BF
type Tape = ([Integer], [Integer])
emptyTape = (repeat 0, repeat 0)
execBFTape :: Tape -> BF -> IO Tape
execBFTape = foldM doBF
execBF :: BF -> IO ()
execBF prog = do
execBFTape emptyTape prog
return ()
doBF :: Tape -> BFInstr -> IO Tape
doBF ((x:lefts), rights) Left = return (lefts, x:rights)
doBF (lefts, (x:rights)) Right = return (x:lefts, rights)
doBF (left, (x:rights)) Inc = return (left, (x+1):rights)
doBF (left, (x:rights)) Dec = return (left, (x-1):rights)
doBF (left, (_:rights)) Input = getChar >>= c -> return (left, fromIntegral (ord c):rights)
doBF t@(_, (x: _)) Output = putChar (chr (fromIntegral x)) >> return t
doBF t@(left, (x: _)) (Loop bf) = if x == 0
then return t
else do t' <- execBFTape t bf
doBF t' (Loop bf)
simpleCommands = [('<', Left),
('>', Right),
(',', Input),
('.', Output),
('+', Inc),
('-', Dec)]
parse :: String -> (BF, String)
parse [] = ([], [])
parse (char:prog) = case lookup char simpleCommands of
Just command -> let (rest, prog') = parse prog
in (command : rest, prog')
Nothing ->
case char of
']' -> ([], prog)
'[' -> let (loop, prog') = parse prog
(rest, prog'') = parse prog'
in (Loop loop:rest, prog'')
_ -> parse prog
所以我有一个类似interpret "[->+<]"
的函数。这给了我一个执行给定程序的IO ()
单元操作。它的类型适合作为某个程序的main
。
假设我想把这个动作编译成一个可执行文件,也就是说,我想生成一个以interpret ...
的结果为主要函数的可执行文件。当然,这个可执行文件必须包含GHC运行时系统(用于无限列表、整数运算等(
问题:
- 我认为,不可能只执行一元操作并将其保存为新文件。这是真的吗
- 如何才能达成类似的解决方案?GHC Api和提示有帮助吗
编辑
对不起,我在原来的问题中过于简单化了。当然,我可以写一个这样的文件:
main = interpret "..."
但当我们试图编译某个东西时,这不是我们通常要做的,所以请考虑interpretFile :: FilePath -> IO ()
。将BF程序保存在一个文件中(helloworld.bf
(。
我该如何创建一个执行helloworld.bf
内容的可执行文件而不需要该文件?
$ ./MyBfCompiler helloworld.bf -o helloworld
答案基本上是否定的。
构造IO
值的方法有很多:
- 内置函数,如
putStrLn
- 类似
return
或>>=
的Monad操作
一旦你有了IO值,有三种方法可以分解它:
- 将
main
设置为等于值 unsafePerformIO
- 作为导出的C函数的返回值
所有这些都分解为将IO a
转换为a
。没有其他方法可以检查它,看看它做了什么。
类似地,对函数唯一能做的就是将它们放入变量中或调用它们(或将它们转换为C函数指针(。
没有其他合理的方法来检查函数。
你可以做的一件事不是编译而是链接,那就是让你的解释器主函数在某个外部c字符串上运行,把它构建成一个静态对象,然后你的"编译器"可以用这个程序的c字符串创建一个新对象,并把它链接到你已经拥有的。
有一种部分求值理论认为,如果你对应用于某些输入的解释器的部分求值器进行部分求值,那么你得到的是编译器,但ghc不是一个足够高级的部分求值程序。
我不确定您是在问如何编写一个可以将helloworld.bf
等文件作为输入的编译器,还是在问如何编译运行helloworld.bf
的Haskell程序。
在前一种情况下,你会想要比这更丰富的东西:
import System.Environment (getArgs)
main :: IO ()
main = do
(_:fileName:_) <- getArgs
source <- readFile fileName
interpret source
interpret :: String -> IO ()
interpret = undefined -- You can fill in this piddly little detail yourself.
如果你想要后者,有几个不同的选择。首先,您可以将*.bf
文件的内容存储在字符串常量中(或者更确切地说,是Text
或严格的ByteString
(,并将其传递给解释器函数。如果GHC足够乐观,能够在编译时完全内联和扩展该调用,我会感到惊讶,但原则上Haskell编译器可以。
第二个是将Brainfuck变成一种特定于领域的语言,使用您定义的运算符,这样您就可以真正编写类似的东西
interpret [^<,^+,^>,^.]
如果定义(^<)
和其他运算符,Brainfuck命令将编译为代表Brainfuc克程序的字节码。
在这种情况下,与第一种方法相比没有明显的好处,但使用更结构化的语言,您可以进行优化,将源代码编译为更适合解释器执行的基于堆栈的字节码,或者生成更复杂的AST。
你也可以将这个想法表达为
interpret
(^< ^+ ^> ^.)
input
这里,如果Brainfuck命令是具有从右到左优先级的高阶函数,并且是interpret bf input = (bf begin) input
,则Brainfuc克代码将简单地编译为解释器调用的函数。这最有可能被转换成快速的本机代码。
上一个答案
在某些情况下,编译器可以内联函数调用(GHC中有一些杂注可以告诉它这样做(。如果您命名闭包,编译器也更有可能执行您想要的操作,例如:
main = interpret foo
在GHC中,您可以通过添加来给编译器一个提示
{-# INLINE main #-}
甚至
{-# INLINE interpret #-}
您可以通过使用-S
编译模块并查看源代码来检查GHC生成了什么代码。