F# to ocaml(3)

初めての衛星中継はアメリカからだった。そして送られて来た映像は、子供心にも ショッキングなものであった。あれから幾年月、悲劇の人を父にする娘さんが、大使と して日本にやって来ると言う。アメリカのブランド戦術が見え隠れする今日この頃であります。

で、その人の決意表明記者会見に、方丈記なんてのが出てきてた。日本人として恥ずかしながら しらんかったぞ。これはいかん、おいらも日本人にならんとな。アプルの本屋へ行ったら ちゃんと無料で読めるやつが置いてあった。こうして、日本人のたしなみを遅まきながら 身に着けようと思ったのです。そして、更に古典って言う事で、源氏物語ですよ。

源氏物語は、1000年の時を経て、いろいろな人が解説してる。与謝野女史、お聖さん、瀬戸内さん等々。 あぷるに置いてあったのは、著作権切れの与謝野バージョン。色々な巻があって、何処から 読み始めれば良いの? 源氏物語攻略本(こういう表現はおかしいかな?)は無いものか?

たまたま、図書館へ行ったら新刊コーナーに『見る・知る・読む 源氏物語』(勉誠出版) なんて言う、おいらにぴったりな本が置いてあったので借りてきた。 早稲田大学の文学名誉教授の中野先生の解説によるもの。

おいら、本はあとがきから読む事にしてる。この本もおいらの流儀に習って読んでみると 衝撃的な事が書かれていた。

とある日本大使(夫妻)が外国へ赴任した。もっかの大使夫人の悩みは、同伴で出席するパーティらしい。 曰く、乾杯が終わってくつろいだ雰囲気になると、相手側夫人が近寄ってきて、源氏物語の 話を仕掛けてくるそうだ。英文科出身の夫人は、源氏が分からないので、グラスを持った まま、逃げ回ることしきりだそうだ。で、旦那はそんな夫人を見て、パーティでは、ちっとも 親睦にならないとこぼしたとか。さぞや、夫人も肩身が狭いでしょうな。

おいらも、そんな事にならないよう、古典に親しむ事にするか。借りてきた本には、巻の 順番(それぞれの巻には桐壷とか夕顔とかのタイトル名が付いている)が載ってたんでメモでも して読書TODOにでも入れておくかな。このTODOも最近は深くなる一方で、絶対にoverflowや underflowを起しません。ただただ、depthが深くなる一方です。

tidi_of_ocaml

次のBugの退治を始める前に、雑然としたソースをちょっと小奇麗にしておこうと思った。 今までemacs上のtuareg-modeを漫然と使ってきたんだけど、どんな機能が隠れているやら 調べておこう。

X上でemacsを使っていれば、メニューをダラーンと下げた一番下にHelpがあるので それを見れば良い。CUIなおいらは、C-h m して、モードの説明を見るんだな。

それぞれのletの書式を整えるには、letの所にカーソルを合わせて、C-c C-q すれば 標準と思われる書式に揃えてくれるよ。letのそれぞれに付いてやらないといけないので、 テストが完了した時点で、小まめにやるのが良いだろう。まとめてやろうとすると 退屈だから。 emacsのマクロを覚えて自動化だな。perl-tidiみたいな外部ツールに 頼るのは、アホなemacs使いだと思われるぞ。後で、キーボードマクロの使い方を調べて おく事。

それから、begin end みたいな雛形を入れる機能があるようだ。おいらは、コメントに なる、(* *) を、自動挿入したいんだけど、そのキーシーケンス、 C-c . c は、 クラスの為に予約されてた。こういう覚え易いのがclassなんて使わない呪文の為に 予約されてるなんて嘆かわしい。事実、結構ocamlを使いこなしている方も、ocamlのoは 忘れていいよって仰ってる。

そんな訳なんで、C-c . c で、コメントの雛形が挿入出来るようにしよう。tuareg.elを 覗いて、それらしいのを探すと、スケルトンなんてのがいろいろと定義されてた。 悪いと承知の上で、次のように書き換え。

(define-skeleton tuareg-insert-class-form
  "Insert a nicely formatted comment form, leaving a mark middle point"
  nil
  "(* " > _ " *)" > \n)

これで、すぐにコメントを書けるようになったよ。以上、改造終了。で、vmの各op-codeに コメントを入れてみた。

   :
  | 28 -> let x = pop () in ports.(x) |> push; ports.(x) <- 0   (* in *)
  | 29 -> let p = pop () in let v = pop () in ports.(p) <- v   (* out *)
  | 30 -> devices ()   (* wait *)
  | _ -> (pushr !ip; jump ()) ;;   (* call by op-data *)

おいらに取っちゃ、op-codeがICのピン番号みたいに見えるぞ。そして矢印は出力端子って 事で、そうすると、コメントは、信号名に思えてくるんで、番号の近くに置きたかった んだ。でも、そうしちゃうとtuaregの整形ルーチンが旨く働かなくなっちゃうんで断念。 何だか、整形も正規表現を駆使してるっぽいので、余り変な事はしない方が、身の為だろう。

何だか、上記がハードの回路図に見えてきたぞ。えっと、デコードの石はSN7442ぐらい かな。あの石は確かBCD-10進のデコーダだったな。ニキシー菅のセグメントドライバーに 使われてて、耐圧不足で良く壊れたっけ。今から40年ぐらい前の話ですな。

コンピュータの回路も、命令のデコーダーの出力ったら、接続先はたいがい各命令の 制御ゲートと決まっていた。(SICPの5章だかに説明が出てたな)よって、自分の持ってる 回路図には、アセンブラの命令名を手書きしてたな。ハードもソフトも一緒だよ。興が乗ったら、 Hello Worldから始めるFPGAなんて のも、本当にハードっぽくて面白いかも知れんな。 以上、じじいの昔話は終了。

ICEまたはロジアナ

さて、前回からの続きで、新しい虫に遭遇します。(vm移植の目標は、retroのプロンプトが 出て来る所まで、ややこしいファイルシステムの扱いはバッサリ省略してます)

[sakae@pcbsd ~/src/ml]$ ocaml
        Objective Caml version 3.12.1

# #use "z.ml" ;;
  :
val exec : unit -> unit = <fun>
- : unit = ()
Exception: Failure "Underflow".
# ip ;;
- : int ref = {contents = 1345}
# memory.(1345) ;;
- : int = 30
# data ;;
- : int list ref = {contents = []}
# address ;;
- : int list ref = {contents = [7651; 7659; 9656]}

Stack underflowエラーを喰らって、落ちてしまいました。原因を、 stackOverflowの皆さんに尋ねてみましょうか。きっと、 袋叩きに遭うだろうな。まずは、自助努力。

トレースしましょ。exec ()の中に、print式を埋め込みました。

let rec exec () =
  if not (!halt) then begin
    print_int !ip; print_string ": "; mon data;
    execOne();
      :
[sakae@pcbsd ~/src/ml]$ ocaml z.ml
0: []
9656: []
7657: []
7659: [-1]
7648: [-1]
7650: [5; -1]
7651: []
1340: []
1342: [0]
1344: [0; 0]
1345: []
Exception: Failure "Underflow".

はて、これが正しいのやら、間違っているのやら、rubyで書かれたvmを標準CPU (正しく動く石の事を、ゴールデン・デバイスって言うそうです)と思って 正解を採取しておきましょ。それには、ICE(イン・サーキット・エミュレータ)かロジアナ を仕掛けます。まあ、ICEも自作しますがね。

# -----------------------------------------------------------------------------
OPTBL = ["nop", "lit", "dup", "drop", "swap", "push", "pop", "call", "jmp",
"rtn", "gt_jmp", "lt_jmp", "ne_jmp", "eq_jmp", "fetch", "store", "add",
"sub", "mul", "divmod", "and", "or", "xor", "lsl", "asr", "z_rtn",
"inc", "dec", "in", "out", "wait"]

def n2op(ip)
  op = @memory[ip]
  ops = op > 30 ? op.to_s : OPTBL[op]
  return ops
end

def ds(x)
  d =x.depth
  return "EMPTY" if d == 0
  rs = "[" ; n =0
  while d > n do rs << (x.nth(n).to_s + ",") ; n = n + 1 end
  return ( rs + "]" )
end
# -----------------------------------------------------------------------------
while @ip < 100000 do
  printf("%d:\t%s\t%s  %s\n", @ip, n2op(@ip), ds(@stack), ds(@address))
  process()
  @ip = @ip + 1
end

ruby.rbの場合、process()ってのが、1命令の実行ルーチンになってるので、その前に リソースの状態表示するように、プローブを埋め込みました。

本当のロジアナだと、機能豊富で、ある命令が出てきたら表示を始めろとか指定出来る んですけど、そういうのは、ばっさり省略。

実行は次のようにします。

[sakae@pcbsd ~/src/ml]$ ruby retro.rb >Hist.log
^Cretro.rb:69:in `getbyte': Interrupt
        from retro.rb:69:in `get_char'
        from retro.rb:111:in `handle_devices'
        from retro.rb:377:in `process'
        from retro.rb:429:in `<main>'

数秒待ってから、強制終了。これで、retroがプロンプトを表示するまでの命令サイクルの ログが取得できました。wc Hist.logしたら、1699行って出てきました。1699ステップを 実行して、コマンド待ちになるんだ。

[sakae@pcbsd ~/src/ml]$ head -n 15 Hist.log
0:      jmp     EMPTY  EMPTY
9656:   7657    EMPTY  EMPTY
7657:   lit     EMPTY  [9656,]
7659:   7648    [-1,]  [9656,]
7648:   lit     [-1,]  [7659,9656,]
7650:   out     [5,-1,]  [7659,9656,]
7651:   1338    EMPTY  [7659,9656,]
1340:   lit     EMPTY  [7651,7659,9656,]
1342:   lit     [0,]  [7651,7659,9656,]
1344:   out     [0,0,]  [7651,7659,9656,]
1345:   wait    EMPTY  [7651,7659,9656,]
1346:   rtn     EMPTY  [7651,7659,9656,]
7652:   lit     EMPTY  [7659,9656,]
7654:   in      [5,]  [7659,9656,]
7655:   rtn     [1000000,]  [7659,9656,]

左から、実行しようとした番地、その命令(数値の場合は、callと解釈されて、飛び先)、dataスタック、 addressスタック。スタックの表示は、左側がTOSにした。

尚、retro側が出してくる文字と、採取出力が、ミキシングされちゃっているんで、ちと わずらわしいけど、解析には便利かな、例えば

[sakae@pcbsd ~/src/ml]$ grep '^[a-z][0-9]' Hist.log
e1346:  rtn     [14,]  [2659,18704,18729,5619,18734,2819,7565,7693,9656,]
t1346:  rtn     [15,]  [2659,18704,18729,5619,18734,2819,7565,7693,9656,]
r1346:  rtn     [16,]  [2659,18704,18729,5619,18734,2819,7565,7693,9656,]
o1346:  rtn     [17,]  [2659,18704,18729,5619,18734,2819,7565,7693,9656,]
o1346:  rtn     [35,]  [2659,18704,18729,5619,18734,2819,8312,8387,]
k1346:  rtn     [36,]  [2659,18704,18729,5619,18734,2819,8312,8387,]

何となく、retro okと読める、1346番地付近は、文字列の出力ルーチンが有るんだな。

debug再開

waitってのは、retroのvm作者さんが付けた命令名だけど、ソースを読む限りでは、 デバイス動作のstartトリガーっぽい。負論理で動作してて、ports.(0)が通常は1、 動作開始の時は0にする。そして、デバイスの状態が定まると1に復帰する。 ポート番号が1以上のものは、それぞれの固定デバイスになってる。

最低限のデバイスを定義したんで、まだ不足が有るかも知れないけど、現状は下記の通り

let devices () =
  if ports.(0) = 0 then  ports.(0) <- 1 ;
  if ports.(1) = 1 then ports.(1) <- key () ;
  if ports.(2) = 1 then
    begin
      ports.(2) <- 0 ;
      let x = pop () in
      if x < 0 then print_char '_'
      else print_char (char_of_int x) ;
    end ;
  ports.(5) <- match ports.(5) with
  | -1 -> mem_size
  | -5 -> List.length (!data)
  | -6 -> List.length (!address)
  | -9 -> halt := true; 0
  | _ -> 0 ;;

落ちていた原因は、posts.(2)の所で、begin .. end で囲んでいなかった為、popが 外部に露出され、実行されちゃたんだ。(本来は、ports.(5)の中のメモリーサイズの 取得リクエストだった。)

こういった、コードを書いた人の意図ととocamlの解釈には乖離が有るんで、 haskellとocamlの両刀使いさん の書かれた戒めを心に刻んでおこう。

次のBUGは新種かな?

   :
18724: [12]
18726: [18470; 12]
18727: [-1; 12]
5436: [-1; 12]
5437: [18727; -1; 12]
   :
1753: [-1; 12]
1754: [-2; 12]
1755: [12]
-1: [12]
Exception: Invalid_argument "index out of bounds".
[sakae@pcbsd ~/src/ml]$ ocaml z.ml | wc -l
Exception: Invalid_argument "index out of bounds".
     130

130歩目で、BUGに出会ったな。雰囲気からすると、過去の過ちが後になって露見したっぽいな。 ロジアナを取り出して、100サイクルから130サイクルぐらいまでをモニター画面に出して みよう。

[sakae@pcbsd ~/src/ml]$ cat -n Hist.log | head -130 | tail -30
    :
   104  18724:  lit     [12,]  [2819,7565,7693,9656,]
   105  18726:  fetch   [18470,12,]  [2819,7565,7693,9656,]
   106  18727:  5434    [-1,12,]  [2819,7565,7693,9656,]
   107  5436:   pop     [-1,12,]  [18727,2819,7565,7693,9656,]
   108  5437:   542     [18727,-1,12,]  [2819,7565,7693,9656,]
     :
   126  5619:   1751    [18729,12,]  [18734,2819,7565,7693,9656,]
   127  1753:   dec     [18729,12,]  [5619,18734,2819,7565,7693,9656,]
   128  1754:   push    [18728,12,]  [5619,18734,2819,7565,7693,9656,]
   129  1755:   rtn     [12,]  [18728,5619,18734,2819,7565,7693,9656,]

何だか違いが発生した所がわかり辛いので、必要な部分を切り取ってきて、上下に 並べてみた。(ocamlの結果をbad.logとして、桁合わせ、期待値をref.logとする)

[sakae@pcbsd ~/src/ml]$ cat bad.log ref.log | sort
   101  1560:           [0; 2548; 12
   101  1560:   swap    [0,2548,12,]  [18723,2819,7565,7693,9656,]
   102  1561:           [0; 2548; 12]
   102  1561:   store   [2548,0,12,]  [18723,2819,7565,7693,9656,]

旨くswapが実行出来ていないな。

let dyadic2 fn = let x, y = fn (pop ()) (pop ()) in push x; push y ;;
 :
  | 4 -> dyadic2 (fun x y -> y, x)   (* swap *)

swapは、内部関数のdyadic2を呼んでるんだけど、pop () の評価順がF#とは逆になってる ようなので、pushの順番を(F#とは)逆にした。一生に一度ぐらいしか、dyadic2なんて 参照しないんでもう忘れる事にしよう。本当は、未来の誰かの為に、コメントを残して おくべきだろうけど。。。

次は、ちょっと先へすすんでサイクル259で止まっているけど、これも過去の過ちがここで暴露 されたってパターンみたい。こんなBugがC言語なんかで発生したら、どうやって追い詰めて 行くんでしょうね? 幸い今回はレコーダーが甘美(なかなかの誤変換)されてるんで、 もぐら叩きみたいに気楽ですけど。

   136  1804:           [82;13]
   136  1804:   rtn     [82,13,]  [18678,18729,5619,18734,2819,7565,7693,9656,]
   137  18679:          [82;13]
   137  18679:  z_rtn   [82,13,]  [18729,5619,18734,2819,7565,7693,9656,]
   138  18680:  dup     [82,13,]  [18729,5619,18734,2819,7565,7693,9656,]
   138  18730:          [82;13]

どうやらz_retが旨く行ってない風。 初期のコードは

  | 25 -> if tos () = 0 then drop (); ip := popr ()   (* z_ret *)

ifが成立した時、popr () は実行されるべきか? まあ、vmの命令自体がretってなってるんで 実行されるべきだよな。じゃ、成立しない時は? うーーーん、無視しなきゃ虫だよね。 って事で、then以下の2つの式を、括弧でくるんで、複合命令化したよ。

今度は、ちゃんと走るかな?

[sakae@pcbsd ~/src/ml]$ ocaml z.ml
bye
Retro 11.6

ok  bye

起動したら、だんまりを決め込まれてしまった。ちゃんと文句を言う大阪人みたいな人の 方が扱い易いんですけど。だんまりはネェー。しょうがないので、byeって入れたら、 つらつらと応答してきたぞ。さよならした後、こっそり文句を言うなんて根暗だなあ。 こういう輩には、一文字づつflushするように、教育してあげたよ。

長くなったので、今日はここまで。Bug取りには、冴えた頭が必要です。 徹夜して仕事が進んだ事なんて全くありませんでした。さっさと帰って、風呂でも 入って、翌朝再チャレンジしたら、悩んでいたのが3分で解決した、なんて経験が ざらにありましたよ。