SML/NJでスタックトレースを表示する


SMLではコード中のほとんどどこからでも例外が投げられるのですが、それがどこから投げられたかを追跡するポータブルな方法は存在しません。

ただしSML/NJでは例外が投げられた時にスタックトレースを表示することが出来たので方法を紹介します。
#なお、ドキュメントは存在しない模様orz

実行例

以下のコードの場合の実行結果を示します。
error() で投げられる例外が、wrap1..4 の呼び出しを巻き戻して上がってきます。

fun error () = (raise Fail "error!";())
fun wrap1 () = error ()
fun wrap2 () = wrap1 ()
fun wrap3 () = wrap2 ()
fun wrap4 () = wrap3 ()

fun main' (_, _) =
   OS.Process.success before wrap4();

fun main (name, argv) =
  BackTrace.monitor(fn () => main'(name, argv))

実行した結果、以下のようなログが得られます。

$ sml @SMLload=stacktrace

*** BACK-TRACE ***
GOTO   stacktrace.sml:7.7-7.42: StackTrace.error[2]
          (from: stacktrace.sml:8.18-8.26: StackTrace.wrap1[2])
GOTO   stacktrace.sml:8.7-8.26: StackTrace.wrap1[2]
          (from: stacktrace.sml:9.18-9.26: StackTrace.wrap2[2])
GOTO   stacktrace.sml:9.7-9.26: StackTrace.wrap2[2]
          (from: stacktrace.sml:10.18-10.26: StackTrace.wrap3[2])
GOTO   stacktrace.sml:10.7-10.26: StackTrace.wrap3[2]
          (from: stacktrace.sml:11.18-11.26: StackTrace.wrap4[2])
CALL   stacktrace.sml:11.7-11.26: StackTrace.wrap4[2]
          (from: stacktrace.sml:14.6-14.39: StackTrace.main'[2])
GOTO   stacktrace.sml:13.7-14.39: StackTrace.main'[2]
          (from: stacktrace.sml:17.32-17.49: StackTrace.main[2])
GOTO   stacktrace.sml:17.23-17.49: StackTrace.main[2]
          (from: stacktrace.sml:17.5-17.50: StackTrace.main[2])
CALL   stacktrace.sml:16.7-17.50: StackTrace.main[2]
          (from: 4120-export.sml:1.38-1.90: XYZ_XXX_0123)

stacktrace.sml:7.25-7.38: Fail: error!

いい感じですね。スタックの底から順に表示されるようです。
main関数から wrap4 の呼び出し以外は全部ジャンプになっているようです。賢い。

使い方

CMファイルで指定する方法と、REPLからその場で使う方法があります。
ちょっと普通と違うので注意。

CMファイルで指定

$smlnj-tdp/plugins.cm を依存関係に追加します。

group
is
  $/basis.cm
  $smlnj-tdp/plugins.cm
  stacktrace.sml

ビルドの段階でTDPモードの指定と、セットアップ用に back-trace.cm を指定します。
この場合、ヒープファイル名の省略は出来ません。

$ ml-build -Ctdp.instrument=true \$smlnj-tdp/back-trace.cm stacktrace.cm StackTrace.main stacktrace

あとは普通に実行するだけ。

$ sml @SMLload=stacktrace
REPLから使用

いきなり $smlnj-tdp/back-trace.cm を読み込んだ後、内部モジュールのフラグを書き換えてから use します(makeではダメ)。

sml> CM.make "$smlnj-tdp/back-trace.cm";
sml> SMLofNJ.Internals.TDP := true;
sml> use "stacktrace.sml";
sml> StackTrace.main(CommandLine.name(), CommandLine.arguments());

..(上に同じ)..