F# to ocaml(4)

トラ技を買ってみた。自分の金で買うなんて何年ぶりだろう? 会社勤めをしてる時は、 会社に置いてあったから買う事も無かったしなあ。自由の身になった時は、興味がソフトに 移ってしまっていて、全く手を出さなかったからなあ。

それが突然興味が出たって事は、特集目当てですよ。デジタル時代のMyラジオ製作 ですって。 RFワールドにも同じようなのが有ったけど、あの本はこちらでは手に入らなかったから、 その反動もあるのかな。

雑誌が薄くなりましたね。広告が減ったから。もう電波少年はいなくなった、それとも 中少企業の技術者も減った? 広告はWebでって事になったのかな。昔、出入りの業者さんに トラ技に広告出すのに幾ら必要なんて興味本位で聞いた事がある。そしたら、最小のヒトコマで 5万円との事だった。自前でサーバー立てて広告を打った方が得だろうね。今のご時勢。

興味深く広告を見たら、TIがチップでも部品を売り出しているのね。最低単位が10個って 事だけど、面白そうな取り組みだね。簡単に試験したダイと、真面目に試験したゴールデン チップと2種類のうちから懐具合で選べるのが面白い。日の丸ICは品質過剰で世界商戦に 敗れた感があるんで、TI流がグローバルスタンダードなんだな。

で、SDRですよ。ハードが安く手に入るようだ。ワンセグ用のドングルがヨーロッパで大量に 消費され、そのおこぼれが東洋にまでやってきたためらしい。

DS-DT305とか ワンセグRX DVB-T+DAB+FM R820T高性能受信機 を使って、ごにょごにょやるみたい。直接パソコンに接続 してもいいし、ラズベリーパイと組み合わせるのがトレンドらしい。 ICOMなんて無線機メーカーのWebにも、「超かんたんSDR(ソフトウエアラジオ)入門」 なんて記事が載るぐらいだから、いつの間にか世間から取り残されているな。

知らないと言えば解説記事の中に有ったんだけど、デシメーションって考え方が人生初見 でしたよ。A/Dの出力を間引き。ただ間引きするんじゃ、何の誤利益も無いんで、積分して 差分を取るらしい。こうする事により、データ量が減って後段のフィルター設計が楽になる。 積分効果により、実質的なA/D分解能が上がるそうだ。

CQ出版からワンセグUSBドングルで作るオールバンド・ソフトウェア・ラジオ なんていうムックが出てたのね。更に、ちょっと高いけど、実験キットが出てて、 その解説本も発売されてるようだ。 また、関連で、 お手軽ARMコンピュータ ラズベリー・パイでI/O なんてのも出てるし、ブームになってるな。rubyも動くようだし、ちょっと遊ぶには 面白いかもしれないな。

電卓ぐらいになるかな?

この間からずっとやってるretroのocaml移植、やっとpromptが出る所まで漕ぎつけた。これで 当初の目的は達した訳だけど、ちょっと欲が出てきた。電卓ぐらいにはなるかな? おっと、その前にvmの各命令のテストカバー率でも調べておくかな。promptが出るまでに 使った命令を比率化しておけば良いだろう。ちょっと長いパイプを施設してみました。 数値(だけのcall命令)は、一応dcallって名前に置き換えておきました。

[sakae@pcbsd ~/src/ml]$ cut -f 2 Hist.log | sed -e 's/[0-9].*/dcall/' | sort | uniq -c | sort -nr
 310 rtn
 302 dcall
 273 lit
 106 swap
  87 push
  74 inc
  71 dup
  70 fetch
  58 pop
  58 dec
  53 z_rtn
  52 out
  44 jmp
  40 drop
  29 eq_jmp
  26 ne_jmp
  23 wait
  13 store
   6 in
   2
   1 xor
   1 sub

命令の種類が30あるうちの20種しか使ってません。(空白の命令は、retroからの改行を 誤カウントしてるためです) 後の10種はまだ未確認。手動でコードを 設定して、単独でテストするって方法も頭を過ぎりましたが、もぐら叩きが楽しそうなので いきなり、vmを働かせてみます。

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

ok  1
1
ok  2
2
ok  +
+
ok  putn
putn Exception: Failure "Underflow".
[sakae@pcbsd ~/src/ml]$ ocaml z.ml
Retro 11.6

ok  1
1
ok  putn
putn A
ok  bye
bye

あれま? putn(TOSを数値と思って表示する)が挙動不審ですよ。化けるのより、落ちるの 方が、直すのが簡単だと思うので、追いかけてみる。まずは、ICEだかロジアナにかけて、 検体を採取だな。

[sakae@pcbsd ~/src/ml]$ ruby retro.rb > Ref.log
1
2
+
putn
^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>'

[sakae@pcbsd ~/src/ml]$ ocaml z.ml > Bad.log
1
2
+
putn
Exception: Failure "Underflow".
[sakae@pcbsd ~/src/ml]$ wc *.log
  114973  344904 11003317 Bad.log
  115454  461792 11682881 Ref.log
  230427  806696 22686198 total

たかが足し算するだけで、こんなに命令を実行してるなんて、ちょっと驚き、ってか、効率が 悪すぎるような気がしますね。まあ、それはさておき、これだけの行数が有ると、相違点を 探すのも至難の業だなあ。こういう汚れ仕事はコンピュータにやらせましょ。相違が 発生した所かそれ以前に問題があるはずだから。。

[sakae@pcbsd ~/src/ml]$ cut -f 1 Ref.log > /tmp/ref
[sakae@pcbsd ~/src/ml]$ cut -f 1 Bad.log > /tmp/bad
[sakae@pcbsd ~/src/ml]$ cmp /tmp/ref /tmp/bad
/tmp/ref /tmp/bad differ: char 11517, line 1889

promptが出て来るまで、約1700ステップを費やしていたから、コマンドを打ち込んで直ぐに 差異が表れたって事か。なんてこったい。

[sakae@pcbsd ~/src/ml]$ cat -n Ref.log | head -n 1890 | tail -n 30 > /tmp/ref
[sakae@pcbsd ~/src/ml]$ cat -n Bad.log | head -n 1890 | tail -n 30 > /tmp/bad
[sakae@pcbsd ~/src/ml]$ cd /tmp
[sakae@pcbsd /tmp]$ cat ref bad | sort
     :
  1888  1755:           [17846;0;49;49;999744]  [5901;5817;17852;13032;17856;17899;19025;19058;19087;4144;4292;4368;8390]
  1888  1755:   rtn     [17846,0,49,49,999744,]  [5901,5817,17852,13032,17856,17899,19025,19058,19087,4144,4292,4368,8390,]
  1889  5902:           [17846;0;49;49;999744]  [5817;17852;13032;17856;17899;19025;19058;19087;4144;4292;4368;8390]
  1889  5904:   1385    [17846,0,49,49,999744,]  [5817,17852,13032,17856,17899,19025,19058,19087,4144,4292,4368,8390,]
  1890  1387:   push    [17846,0,49,49,999744,]  [5904,5817,17852,13032,17856,17899,19025,19058,19087,4144,4292,4368,8390,]
  1890  5903:           [17846;0;49;49;999744]  [5817;17852;13032;17856;17899;19025;19058;19087;4144;4292;4368;8390]

おいらの80桁端末では、行長すぎて、折り返し表示されてしまい、おちおち見てられん。 Xを上げて、横に広い端末を作ってもいいんだけど、おいらは昔の人。80桁端末で快適に 表示する方法は無いかな?

ふと、スタックの全データを表示するなんて無駄じゃん。大体、演算の対象がスタック上の 2つのデータしかないんだから、せいぜい3個ぐらいのデータ表示で十分と気付く。

let mon3 rx =
  let si i = string_of_int i
  and sis i = string_of_int i ^ "; "
  in match !rx with
    [] -> "[]"
  | [a] -> "[" ^ si a ^ "]"
  | [a; b] -> "[" ^ sis a ^ si b ^ "]"
  | [a; b; c] -> "[" ^ sis a ^ sis b ^ si c ^ "]"
  | a::b::c::_ -> "[" ^ sis a ^ sis b ^ sis c ^ "..]"

let rec exec () =
  if not (!halt) then
    begin
      Printf.printf "%d:\t\t%s  %s\n" !ip (mon3 data) (mon3 address) ;
      execOne();
      ip := !ip + 1 ;
      exec ()
    end ;;

letで宣言してる内部関数、無くてもいいんだけど、入力文字数短縮の為。ocamlの関数名を 決めた人、センス悪すぎ。まあ、フランス人に英語で考えろって言ったら見紛えちゃうだろう から無理は無いか。ocamlにもprintfなんてのが有った。不定個の引数を関数型言語はどうやって 処理してるのだろう? 後でPrintfモジュールを覗いておこう。

def ds3(x)
  def is(x, n) x.nth(n).to_s end
  def iss(x, n) x.nth(n).to_s + ", " end
  case x.depth
    when 0 then
      "[]"
    when 1 then
      "[" + is(x,0) + "]"
    when 2 then
      "[" + iss(x,0) + is(x,1) + "]"
    when 3 then
      "[" + iss(x,0) + iss(x,1) + is(x,2) + "]"
  else
      "[" + iss(x,0) + iss(x,1) + iss(x,2) + "..]"
  end
end

rubyでmatchもどきをやろうとしたら、こうなったぞ。 Rubyによる関数型プログラミング なんてのと対比すると、面白いかも知れない。

とまあ、道具を作って、今出てる虫を眺めてみたんだけど、虫取り方法がピンとこないので 別アプローチしてみる。そもそも、おいらはプロンプトが出て来るまで移植が済んだら、 ベンチをやってみたかったんだ。目的へ一直線でgoするか。

ocamlは早いのか?

ってな訳で、retroのベンチで作っておいたloop用のROM(retroImage)を持ってきた。 走らせてみると

[sakae@pcbsd ~/src/ml]$ ocaml z.ml
Exception: Failure "hd".

あらま、虫が沸いてきた、と言うより新たなretroImageに隠れて虫が侵入きたんだな。 そして繁殖したとな。クリーンなCPUのそれと比べてみよう。

   304  5819:           [23047]  [14237; 1000; 5619; ..]
   304  5819:   rtn     [23047]  [14237, 1000, 5619, ..]
   305  14238:          [23047]  [1000; 5619; 14248; ..]
   305  14238:  pop     [23047]  [1000, 5619, 14248, ..]
   306  14239:          [1000; 23047]  [5619; 14248; 23048; ..]
   306  14239:  loop    [1000, 23047]  [5619, 14248, 23048, ..]
   307  14235:          [23047]  [5619; 14248; 23048; ..]
   307  14235:  push    [999, 23047]  [5619, 14248, 23048, ..]
   308  14236:          []  [23047; 5619; 14248; ..]
   308  14236:  dup     [23047]  [999, 5619, 14248, ..]

空のスタック(トップ)を複製しようとして、落ちたんだな。loop命令の後ocamlのvmの方は スタックが一段浅くなってる。call内に虫が潜んでいるな。

loop命令は外にくくり出したloopマクロに丸投げしてた。じっと眼をこらしていたら虫が 見えたぞ。括弧忘れで虫が逃げ出したんだな。毎度、カッコ悪い。

[sakae@pcbsd ~/src/ml]$ time ocaml z.ml
real    0m41.562s
user    0m40.706s
sys     0m0.134s

以前mono上でF#でやった時には、46秒だったから、ocamlの方が速いな。

[sakae@pcbsd ~/src/ml]$ ocamlopt z.ml
[sakae@pcbsd ~/src/ml]$ time ./a.out
real    0m7.349s
user    0m7.220s
sys     0m0.064s

やっとコンパイルして実行だ。mono上では13秒、C言語だと4秒だったな。

出来上がったa.outは、極々普通なもの

[sakae@pcbsd ~/src/ml]$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), for FreeBSD 9.1, not stripped
[sakae@pcbsd ~/src/ml]$ ldd a.out
a.out:
        libm.so.5 => /lib/libm.so.5 (0x2807f000)
        libc.so.7 => /lib/libc.so.7 (0x28099000)

ocamloptでコンパイルする時に、-Sを付けるとアセンブラ出力が得られる。 アセンブリ具合も見ておくか

        .text
        .align  16
        .globl  camlZ__incIp_1083
camlZ__incIp_1083:
.L207:
        movl    camlZ + 12, %eax
        addl    $2, (%eax)
        movl    $1, %eax
        ret
        .type   camlZ__incIp_1083,@function
        .size   camlZ__incIp_1083,.-camlZ__incIp_1083

メインが何処に有るか探したけど見つからず。しょうがないので、-gを付けてコンパイル したやつをgdbに喰わせてみた。 適当な所で止めて、呼び出し具合を確認。

[sakae@pcbsd ~/src/ml]$ gdb a.out
GNU gdb 6.1.1 [FreeBSD]
  :
(gdb) b camlZ__loop_1089
Breakpoint 1 at 0x804a280
(gdb) run
Starting program: /usr/home/sakae/src/ml/a.out
(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...
Breakpoint 1, 0x0804a280 in camlZ__loop_1089 ()
(gdb) l
No symbol table is loaded.  Use the "file" command.
(gdb) bt
#0  0x0804a280 in camlZ__loop_1089 ()
#1  0x0804a7fa in camlZ__exec_1100 ()
#2  0x0804a9d2 in camlZ__entry ()
#3  0x0804963d in caml_startup__code_begin ()
#4  0x0805bb76 in caml_start_program ()
#5  0x00000000 in ?? ()
#6  0xbfbfe7a8 in ?? ()
#7  0xbfbfe80c in ?? ()
#8  0x00000001 in ?? ()
#9  0x08049620 in frame_dummy ()
Previous frame inner to this frame (corrupt stack?)

camlZ__entryがおいらの書いたコードへの入り口、いわゆるtoplevelの環境を実現してる。 このルーチンの最後はretになってた。ocamlが用意したcrt.o相当へ帰って行くんだな。

BUG入りソース

いいかげん飽きてきたんで、BUG入りだけど移植したソースを掲げて置きます。昆虫食が 好きな方はどうぞ味わってみて下さい。

(* import retro.fsx *)

let mem_size = 1_000_000
let image_file = "retroImage"
let ports_size = 12

let ip = ref 0
let ports = Array.make ports_size 0
let memory = Array.make mem_size 0xdeadbee
let halt = ref false

let data = ref []
let address = ref []

let pushVal d x =    d := x :: !d
let popVal d () = match !d with h :: t -> d := t; h | _ -> failwith "Underflow"
let push = pushVal data
let pushr = pushVal address
let pop = popVal data
let popr = popVal address
let tos () = List.hd (!data)

let (|>) (x: int) f = f x ;;

(* for debug, monitor data or address stack *)
let mon3 rx =
  let si i = string_of_int i
  and sis i = string_of_int i ^ "; "
  in match !rx with
    [] -> "[]"
  | [a] -> "[" ^ si a ^ "]"
  | [a; b] -> "[" ^ sis a ^ si b ^ "]"
  | [a; b; c] -> "[" ^ sis a ^ sis b ^ si c ^ "]"
  | a::b::c::_ -> "[" ^ sis a ^ sis b ^ sis c ^ "..]"

let load () =
  let b4 = Array.make 4 0
  in let binReader = open_in_bin image_file
     in for i = 0 to  (in_channel_length binReader / 4  - 1) do
       b4.(0) <- input_byte binReader;   (* LSB *)
       b4.(1) <- input_byte binReader;
       b4.(2) <- input_byte binReader;
       b4.(3) <- input_byte binReader;
       memory.(i) <- b4.(3) lsl 24 + b4.(2) lsl 16 + b4.(1) lsl 8 + b4.(0)
       done;
     close_in binReader ;;

let key () = input_byte stdin ;;

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) ; flush stdout; )
    end ;
  ports.(5) <- match ports.(5) with
  | -1 -> mem_size
  | -5 -> List.length (!data)
  | -6 -> List.length (!address)
  | -9 -> halt := true; 0
  | _ -> 0 ;;

let dyadic fn = let x = pop () in fn (pop ()) x |> push ;;
let dyadic2 fn = let x, y = fn (pop ()) (pop ()) in push x; push y ;;
let incIp () = ip := (!ip) + 1 ;;
let condJump fn =
  let x = pop () in
  if fn (pop ()) x then ip := memory.(!ip + 1) - 1 else incIp () ;;
let jump () =
  ip := memory.(!ip) - 1 ;
  if memory.(!ip + 1) = 0 then incIp () ;
  if memory.(!ip + 1) = 0 then incIp () ;;
let drop () = pop () |> ignore ;;
let loop () =
  pop () - 1 |> push ;
  if tos () > 0 then ip := memory.(!ip + 1) - 1
  else (incIp (); drop ()) ;;

let execOne () =
  match memory.(!ip) with
    0 -> ()   (* nop *)
  | 1 -> ( incIp (); memory.(!ip) |> push )   (* lit *)
  | 2 -> tos () |> push   (* dup *)
  | 3 -> drop ()   (* drop *)
  | 4 -> dyadic2 (fun x y -> y, x)   (* swap *)
  | 5 -> pop () |> pushr   (* push *)
  | 6 -> popr () |> push   (* pop *)
  | 7 -> loop ()   (* loop *)
  | 8 -> ( incIp (); jump () )   (* jmp *)
  | 9 -> ip := popr ()   (* rtn *)
  | 10 -> condJump (>)   (* gt_jmp *)
  | 11 -> condJump (<)   (* lt_jmp *)
  | 12 -> condJump (<>)  (* ne_jmp *)
  | 13 -> condJump (=)   (* eq_jmp *)
  | 14 -> memory.(pop ()) |> push   (* fetch *)
  | 15 -> let p = pop () in let v = pop () in memory.(p) <- v   (* store *)
  | 16 -> dyadic (+)   (* add *)
  | 17 -> dyadic (-)   (* sub *)
  | 18 -> push (pop () * pop ())   (* mul *)
  | 19 -> dyadic2 (fun x y -> y / x, y mod x)   (* divmod *)
  | 20 -> dyadic (land)   (* and *)
  | 21 -> dyadic (lor)   (* or *)
  | 22 -> dyadic (lxor)  (* xor *)
  | 23 -> dyadic (lsl)   (* lsl *)
  | 24 -> dyadic (asr)   (* asr *)
  | 25 -> if tos () = 0 then (drop (); ip := popr ())   (* z_ret *)
  | 26 -> pop () + 1 |> push   (* inc *)
  | 27 -> pop () - 1 |> push   (* dec *)
  | 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 *)

let rec exec () =
  if not (!halt) then
    begin
      Printf.printf "%d:\t\t%s  %s\n" !ip (mon3 data) (mon3 address) ;
      execOne();
      ip := !ip + 1 ;
      exec ()
    end ;;

load () ;;
exec () ;;