如何在使用GHC编译Haskell时禁用"loop"异常?

4
如果我的程序出现无限循环,我希望它实际上被卡住:无限执行、耗尽内存或因堆栈溢出而崩溃。
我不想立即退出并显示<>错误消息。如何禁用运行时的无限循环检测?

5
我猜可以捕获异常并进行一些不被检测到的循环,例如 let f () = f () in f () 这个方法就不会被黑洞机制检测到。我不知道是否有选项可以完全禁用黑洞机制。通常,它是一个有用的功能。 - chi
2
请注意,您只能在IO内部捕获它,例如在程序的“main”或GHCi提示符中。您能详细说明为什么要禁用黑洞吗? - chi
2
@leftaroundabout 我不是很确定。例如在安全相关的应用中,当您应该循环时过早终止可能会导致信息泄露。 - Daniel Wagner
7
我非常想知道为什么有人会想要这个...你能提供一个使用案例吗?我唯一能想到的是人们倾向于在C程序末尾添加for (;;);,以便在Windows上运行时终端保持足够长的时间让他们读取输出。如果您正在做这件事,那么在 Haskell 和 C 中有更好的方法。 - DarthFennec
1
如果使用-threaded编译并使用+RTS -N2 -RTS运行,会发生什么?线程化运行时通常以不同的方式处理黑洞(作为灰洞),因为其他线程可能正在评估该thunk。 - dfeuer
显示剩余4条评论
2个回答

3
这是一个可行但不太好的方法。当检测到黑洞循环并创建了一个非终止异常时,您可以为该异常创建一个虚假的信息表并使其循环。

假设您有一个Haskell程序:

-- Loop.hs
foo :: Int
foo = foo
main = print $ foo

如果您使用以下方式编译:
$ ghc -O2 Loop.hs

如果你运行它,它会生成一个Loop: <<loop>>错误。

但是,如果你创建一个汇编文件:

# halt_and_catch_fire.s
# version for Linux x86_64 only
.globl base_ControlziExceptionziBase_nonTermination_closure
.section .data
.align 8
base_ControlziExceptionziBase_nonTermination_closure:
    .quad loop
.section .text
.align 8
    .quad 0
    .quad 14            # FUN_STATIC
loop:   jmp loop

然后使用适当的链接器标志将其与您的Haskell程序编译在一起(以忽略重复定义):

$ ghc -O2 Loop.hs halt_and_catch_fire.s -optl -zmuldefs

如果运行它,它会锁定。
请注意,上述汇编适用于x86_64 Linux。在任何其他体系结构(包括32位Linux)上,都需要进行修改,因为闭包布局非常依赖于体系结构。

2
这不是一个完整的答案,但无法放在评论中。在这种情况下,应该询问无限循环检测是什么以及它如何工作。我将通过将Haskell转换为类似Haskell的语言,并使用严格(非惰性)评估(例如使用Haskell语法的JavaScript)来解释它。
let f () = f () in f ()

转换为严格模式:

let f () = make_thunk (\() -> eval_thunk f ()) in eval_thunk (make_thunk (\() -> f ()))

现在让我们进行一次优化:
let f () = eval_thunk f_unit
    f_unit = make_thunk (\() -> f ())
in
eval_thunk f_unit

现在,让我们写一个假定义来解释“thunks”:

data ThunkInner a = Done a | Fresh (() -> a) | Working
data Thunk a = Thunk { mutable inner :: ThunkInner a }
make_thunk f = Thunk (Fresh f)
eval_thunk t = case t.inner of
  | Done a -> a
  | Fresh f -> (t.inner <- Working; let result = f () in t.inner <- Done result; result)
  | Working -> error "<<loop>>"

因此,当我们尝试计算某些值时,会出现无限循环。您可以手动跟踪上面的程序,以了解如何出现此错误。
但是,如果没有进行优化怎么办?那么(假设您不将其优化为严格模式),您将得到堆栈溢出,并将其报告为<>错误。如果它被优化为真正的严格模式,那么也许您将得到堆栈溢出错误,或者可能会出现无限循环。
有人可能会问如何避免这种错误,答案可能是:
  1. 不要编写无限循环
  2. 可能编译时不要进行优化
但您可能会说有时候无限循环是有用的,例如事件循环或长时间运行的服务器。回答是在Haskell中,无限循环并不实用,因为要想变得实用,需要使用IO。考虑一个函数be_useful :: IO (),现在我们可以写成:
f = be_useful >>= \() -> f

现在执行f有两个步骤:

  1. 评估thunk
  2. 执行该值中的IO操作
  3. 执行下一个IO操作(重新计算thunk)以此类推。

这不需要<<loop>>


感谢您的评论! - Jesbus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接