ES-BASIC (ver.0.2b時点) でのデバッグ機能について

  • (by K, 2020.05.16)

(0) はじめに

  • ES-BASICは簡易デバック機能があります。その活用事例を紹介します。

(1) オーバーフロー自動検出の例

  • 数学には組み合わせ数の計算としてコンビネーションという計算があります。
  • そのコンビネーションを使って 20C10 の値を計算してみたいと思います。
  • これは
    (20*19*18*17*16*15*14*13*12*11)/(10*9*8*7*6*5*4*3*2*1)
  • で計算できる値です。
  • これが実際どんな値になるのかというと 184756 になります。
  • 当然ですがこの結果は32ビット整数に余裕で収まる値です。
  • ということで、32ビット版のES-BASIC(esbasic32.exe)でも余裕で計算できるだろうと考えて、
    1000 A=1
    1010 FOR I=11,20
    1020   A=A*I
    1030 NEXT
    1040 B=1
    1050 FOR I= 1,10
    1060   B=B*I
    1070 NEXT
    1080 PRINT "20C10=",A/B
  • というプログラムを書いてみました。
  • そして実行しました。出てきた結果は・・・
    Ok
    RUN
    20C10=117
    
    Ok
  • となってしまいます!・・・はい、おかしいですね。見事にバグりました。
    • ちなみに、32ビットのC言語で同じアルゴリズムで計算しても同じ結果になります。そういう意味では忠実に計算できているとも言えます。

  • さてこんな時、ES-BASICではどうするのでしょうか。はい、LWAIT命令を使って実行速度を下げます。
    • LWAIT命令は、line-waitという意味で、つまり1行実行するごとにwaitを入れて実行速度を下げるのです。
    • 実行が速すぎて何が起きているのかよくわからなくなった時のために準備されたコマンドなのですが、この命令で速度を下げると、自動的に各種のデバッグ支援機能が全部ONになるようになっています。
  • ということで、実行速度を下げてから再度RUNしてみます。
    Ok
    LWAIT 0
    
    Ok
    RUN
    
    exectime error
    break in 1020
    
    Ok
  • お、なんかエラーで止まってくれました。実行時エラーです。1020行で止まったらしいです。
    • LWAIT 0 は行ウェイトが0で減速しないのですが、デバッグ機能は全部ONになるモードです。これでもデフォルトの LWAIT -1 (デバッグ機能OFF)よりは遅くなっています。
  • どのくらい実行してから止まったのか、まずはそこを観察してみることにします。LINECOUNTER命令を使います。これで行ごとの実行回数がわかります。
    Ok
    LINECOUNTER
    0000000000000001  1000 A=1
    0000000000000001  1010 FOR I=11,20
    0000000000000009  1020   A=A*I
    0000000000000008  1030 NEXT
    0000000000000000  1040 B=1
    0000000000000000  1050 FOR I= 1,10
    0000000000000000  1060   B=B*I
    0000000000000000  1070 NEXT
    0000000000000000  1080 PRINT "20C10=",A/B
    
    Ok
  • おお、1010~1030のFORループの途中で死んでしまったようです。では、AやIの値を確認してみることにします。
    Ok
    PRINT A,,I
    1764322560 19
    
    Ok
  • なるほどー。どうやらA=176432250でI=19のときに、A*Iを計算して、オーバーフローになっているようです。
    • [註] 11*12*13*14*15*16*17*18 は 176432250 になりますので、ここまでの計算はあっています。
  • ということで、簡単にバグ原因を突き止めることができました。めでたしめでたし。

  • [Q] 最初から LWAIT 0 にしておけば、もっと早くこのバグに気づけたのではないか?
    • [A] 全くその通りです。だからデフォルトを LWAIT 0 にしておいて、ずっとそのまま使い続けるほうが親切かもしれません。
    • しかし私は、スピード狂でもあるので(笑)、しょっちゅう LWAIT -1 に戻してしまいます。ということで自分のためには LWAIT -1 をデフォルトにする方が使いやすいと思って、今はそうしています。
  • [Q] 64bit版のES-BASICでは問題なく正しい答えがでてしまうが、それでいいのか?
    • [A] 64bit版のES-BASICは64bit整数を使っているので、問題なく正しい答えが出ることになります。
    • ここで32bitのES-BASICを例に出したのは、32bit版なら64bit対応Windowsの人も32bit対応Windowsの人も、同じように試してみることができるからです。
  • [Q] 64bit版のES-BASICでも、結果が64bit整数に収まらないような演算をしたら実行時エラーで教えてくれるのか?
    • [A] はい、LWAIT 0 以上であれば、ちゃんと教えてくれます。
  • [Q] 場合によってはオーバーフローしても無視して続行させたいケースもあるかもしれないが、そのケースは考慮されているのか?
    • [A] はい。そういうこともあると思って、「 LWAIT 0 以上でも、この演算についてはオーバーフローを検出するな」という記述を可能にしてあります。でも私自身はその機能を使ったことはまだありません。

(2) 無限ループに陥ってしまった場合の例

  • これは私がesbasic02aの(5-4)の迷路作成プログラムを作っていた時に遭遇したバグです。
    1000 $RANDSEED=1; GETARG $RANDSEED
    1005 ALIAS XY:R03, T:R06; // これを付けるとサイズを小さくできる
    1010 ARY INT NEW A[1504]
    1020 FOR T=0,1503; A[T]=1; NEXT
    1030 A[33]=0; // 1<<5|1 // 左上に一つだけ穴をあける
    1040 FOR I=0,1000000
    1050   X=(RAND%23)*2+1
    1060   Y=(RAND%15)*2+1
    1070   XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    1080     DOLOOP
    1090       D0=0; D1=0; D2=0; D3=0
    1100       IF X<45 THEN D0=A[XY+32]; T=A[XY+64]; D0=D0&T; FI
    1110       IF X> 1 THEN D1=A[XY-32]; T=A[XY-64]; D1=D1&T; FI
    1120       IF Y<29 THEN D2=A[XY+ 1]; T=A[XY+ 2]; D2=D2&T; FI
    1130       IF Y> 1 THEN D3=A[XY- 1]; T=A[XY- 2]; D3=D3&T; FI
    1140       T=D0+D1+D2+D3
    1150       IF T==0 GOTO BRK; D=T
    1160       T=RAND%D
    1170       IF D0!=0 THEN IF T==0 THEN A[XY+32]=0; X=X+2; FI; T=T-1; FI
    1180       IF D1!=0 THEN IF T==0 THEN A[XY-32]=0; X=X-2; FI; T=T-1; FI
    1180       IF D2!=0 THEN IF T==0 THEN A[XY+ 1]=0; Y=Y+2; FI; T=T-1; FI
    1200       IF D3!=0 THEN IF T==0 THEN A[XY- 1]=0; Y=Y-2; FI; T=T-1; FI
    1210       XY=X<<5|Y; A[XY]=0;
    1220     ENDDO
    1230   FI
    1240 BRK:
    1250 NEXT
    1260 FOR Y=0,30
    1270   FOR X=0,46
    1280     XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    1290       PRINTF "  "
    1300     ELSE
    1310       PRINTF "##"
    1320     FI
    1330   NEXT
    1340   PRINTF "\n"
    1350 NEXT
  • 一見するとどこも間違っているようには見えません。
  • それなのに実行すると・・・迷路は出力されず、無限ループっぽい感じになります(実際は本当に無限ループしています)。

  • さてどうしましょう。はい、 LWAIT 0 にして実行することにします。
  • それでも当然ですが無限ループすることに変わりはありません。ということで、まずグラフィックウィンドウをアクティブにして「Shift+Home」を入力します。
    Ok
    RUN
    
    pause in 1110
    
    (Ok)
  • なんか変なプロンプトが出ました。これはプログラムを一時的に中断していることを意味します。
  • まずは毎度の LINECONTER コマンドです。
    (Ok)
    LINECOUNTER
    0000000000000001  1000 $RANDSEED=1; GETARG $RANDSEED
    0000000000000001  1005 ALIAS XY:R03, T:R06; // これを付けるとサイズを小さくできる
    0000000000000001  1010 ARY INT NEW A[1504]
    0000000000000001  1020 FOR T=0,1503; A[T]=1; NEXT
    0000000000000001  1030 A[33]=0; // 1<<5|1 // 左上に一つだけ穴をあける
    0000000000000001  1040 FOR I=0,1000000
    0000000000000187  1050   X=(RAND%23)*2+1
    0000000000000187  1060   Y=(RAND%15)*2+1
    0000000000000187  1070   XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    0000000000000002  1080     DOLOOP
    000000000197887b  1090       D0=0; D1=0; D2=0; D3=0
    000000000197887b  1100       IF X<45 THEN D0=A[XY+32]; T=A[XY+64]; D0=D0&T; FI
    000000000197887b  1110       IF X> 1 THEN D1=A[XY-32]; T=A[XY-64]; D1=D1&T; FI
    000000000197887a  1120       IF Y<29 THEN D2=A[XY+ 1]; T=A[XY+ 2]; D2=D2&T; FI
    000000000197887a  1130       IF Y> 1 THEN D3=A[XY- 1]; T=A[XY- 2]; D3=D3&T; FI
    000000000197887a  1140       T=D0+D1+D2+D3
    000000000197887a  1150       IF T==0 GOTO BRK; D=T
    0000000001978879  1160       T=RAND%D
    0000000001978879  1170       IF D0!=0 THEN IF T==0 THEN A[XY+32]=0; X=X+2; FI; T=T-1; FI
    0000000001978879  1180       IF D2!=0 THEN IF T==0 THEN A[XY+ 1]=0; Y=Y+2; FI; T=T-1; FI
    0000000001978879  1200       IF D3!=0 THEN IF T==0 THEN A[XY- 1]=0; Y=Y-2; FI; T=T-1; FI
    0000000001978879  1210       XY=X<<5|Y; A[XY]=0;
    0000000001978879  1220     ENDDO
    0000000000000000  1230   FI
    0000000000000185  1240 BRK:
    0000000000000186  1250 NEXT
    0000000000000000  1260 FOR Y=0,30
    0000000000000000  1270   FOR X=0,46
    0000000000000000  1280     XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    0000000000000000  1290       PRINTF "  "
    0000000000000000  1300     ELSE
    0000000000000000  1310       PRINTF "##"
    0000000000000000  1320     FI
    0000000000000000  1330   NEXT
    0000000000000000  1340   PRINTF "\n"
    0000000000000000  1350 NEXT
    
    (Ok)
  • 言い忘れましたが、 LINECOUNTER のカウンタは16進数表示になっています(できるだけ少ない桁数で表示したかったので)。
  • でもこれだけでは無限ループなのかどうかいまいち判断できません。だから RESUME コマンドで処理を再開して、少ししてからもう一度「Shift+Home」します。
    (Ok)
    RESUME
    
    pause in 1180
    
    (Ok)
    LINECOUNTER
    0000000000000001  1000 $RANDSEED=1; GETARG $RANDSEED
    0000000000000001  1005 ALIAS XY:R03, T:R06; // これを付けるとサイズを小さくできる
    0000000000000001  1010 ARY INT NEW A[1504]
    0000000000000001  1020 FOR T=0,1503; A[T]=1; NEXT
    0000000000000001  1030 A[33]=0; // 1<<5|1 // 左上に一つだけ穴をあける
    0000000000000001  1040 FOR I=0,1000000
    0000000000000187  1050   X=(RAND%23)*2+1
    0000000000000187  1060   Y=(RAND%15)*2+1
    0000000000000187  1070   XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    0000000000000002  1080     DOLOOP
    0000000002db8218  1090       D0=0; D1=0; D2=0; D3=0
    0000000002db8218  1100       IF X<45 THEN D0=A[XY+32]; T=A[XY+64]; D0=D0&T; FI
    0000000002db8218  1110       IF X> 1 THEN D1=A[XY-32]; T=A[XY-64]; D1=D1&T; FI
    0000000002db8218  1120       IF Y<29 THEN D2=A[XY+ 1]; T=A[XY+ 2]; D2=D2&T; FI
    0000000002db8218  1130       IF Y> 1 THEN D3=A[XY- 1]; T=A[XY- 2]; D3=D3&T; FI
    0000000002db8218  1140       T=D0+D1+D2+D3
    0000000002db8218  1150       IF T==0 GOTO BRK; D=T
    0000000002db8217  1160       T=RAND%D
    0000000002db8217  1170       IF D0!=0 THEN IF T==0 THEN A[XY+32]=0; X=X+2; FI; T=T-1; FI
    0000000002db8217  1180       IF D2!=0 THEN IF T==0 THEN A[XY+ 1]=0; Y=Y+2; FI; T=T-1; FI
    0000000002db8216  1200       IF D3!=0 THEN IF T==0 THEN A[XY- 1]=0; Y=Y-2; FI; T=T-1; FI
    0000000002db8216  1210       XY=X<<5|Y; A[XY]=0;
    0000000002db8216  1220     ENDDO
    0000000000000000  1230   FI
    0000000000000185  1240 BRK:
    0000000000000186  1250 NEXT
    0000000000000000  1260 FOR Y=0,30
    0000000000000000  1270   FOR X=0,46
    0000000000000000  1280     XY=X<<5|Y; T=A[XY]; IF T==0 THEN
    0000000000000000  1290       PRINTF "  "
    0000000000000000  1300     ELSE
    0000000000000000  1310       PRINTF "##"
    0000000000000000  1320     FI
    0000000000000000  1330   NEXT
    0000000000000000  1340   PRINTF "\n"
    0000000000000000  1350 NEXT
    
    (Ok)
  • ということで、どうやら何らかの理由で、1080~1220の DOLOOP から抜け出せなくなったようです。
  • じゃあちょっと状況を確認してみることにします(改行幅のせいで見にくくてすみません。実画面ではもっと見やすいです)。
  • (こういう状況でカジュアルに実行できるところが、ただのデバッガよりも格段に便利なところだと思っています。)
    (Ok)
    PRINT X,,Y
    25 3
    
    (Ok)
    FOR Y=0,30; FOR X=0,46; XY=X<<5|Y; T=A[XY]; IF T==0 THEN PRINTF "  "; ELSE PRINTF "##"; FI; NEXT; PRINTF "\n"; NEXT
    ##############################################################################################
    ##          ######      ##      ##      ##          ##      ##      ######              ##  ##
    ##########  ######  ##  ##  ##  ##  ##  ##  ######  ##  ##  ##  ##  ######  ##########  ##  ##
    ##########      ##  ##      ##      ##  ##  ######  ##  ##      ##      ##  ##########      ##
    ##############  ##  ##################  ##  ##########  ##############  ##  ##################
    ##############      ##################  ##  ##          ##############  ##  ##################
    ######################################  ##  ##  ######################  ##  ##################
    ######################################          ######################      ##################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    ##############################################################################################
    
    (Ok)
  • これで、状況が分かりました。どうやら穴掘りで左に曲がらなければいけないときに、曲がれなくなってしまったようです。
  • ・・・でも、なぜ??
  • ということで、プログラムを確認してみると・・・あーー!1180行が2つあって1190行がない!!上書きしてしまったのか!
  • ということで、行番号を打ち間違えるという、実に恥ずかしいミスだったのです。ちゃんちゃん。
  • コンソールでEND命令を実行して、プロンプトを「Ok」に戻してから、プログラムを修正して、RUNするとうまくいくようになります。

  • [Q] まあまあ面白かった。ところで、デバッガのようなステップ実行はできるのか?
    • [A] 今のところ、自分の利用ケースでステップ実行が必要になったことがないこともあり、まだステップ実行はできるようにしていません。まあ必要になったらすぐにつけられそうな気はしていますが。
    • その代わり、怪しい処理に差し掛かるまでは LWAIT 0 で実行して、途中で「Shift+Home」して、そこで LWAIT 1000000 とかにして、ゆっくり実行させて、ここぞというところで「Shift+Home」する、みたいなことは想定しています。
    • またプログラム中でLWAIT命令を実行することもできるので、怪しいところになったら自動で減速することも可能です。

(3) 検出できる実行時エラー

  • オーバーフロー(参考:(1))
  • 配列アクセスで宣言範囲を超えたアクセスをしようとした場合
  • ゼロ除算、ゼロ剰余算しようとしたとき
  • 現状では、この程度しかなくてすみません。メモリをmallocしたりfreeしたりできるようになったら、double-freeとかuse-after-freeとかヌルポインタアクセスなども検出できるようにしたいです。
  • また未初期化変数参照とかもいつかやりたいなと思っています。

(4) LWAIT 命令の考え方

  • ES-BASICでは、各種の実行時エラー検出をON/OFFできるようにするという方針で作っています。普通に考えれば、常にONにすべきだと思うかもしれません。そのほうが開発者にやさしい(=ポカミスに早く気付く、見逃さない)というのは間違いありません。
  • しかしそうなるとどうしても実行速度は落ちます。実行速度が落ちると、実行速度が求められるケースではES-BASICを避けるようになってしまいます。そしてデバックに泣かされるのです。それじゃあ効果は薄いと思いました。だからチェックをOFFにして高速に実行できるモードはあるべきだと考えています。
  • また高速に実行できるモードがあるからこそ、実行時エラーのチェックは容赦なくできるようになりました。なぜなら、もし高速モードがなかったら「これはチェックしたほうがしないよりはいいけど、でもこんなのめったにしないミスだからなあ、そんなもののためにチェックに時間がかかったらほとんどのケースでうれしくないんじゃないかなあ」などと思って、処理系でのチェックをためらうようになってしまうのです。でもそれじゃあ魅力半減です。徹底的に自動チェックしてくれるからこそ、デバッグ効率が大幅に改善するのです。それもこれも、「どうせ必要ない時はOFFにできるのだから、ONのときは容赦なくいこう!」と思えるからこそなのです。

(9) 感想

  • 私は、普段はデバッガなどを使わずに、基本的にprintfデバッグをやっています(笑)。
  • だからこそなのかもしれませんが、この ES-BAISC のデバッグ環境は、私にとってはあまりに魅力的で、病みつきになっています。
  • ES-BASICの基本的な考えとして、「デバッグ時と通常時でコマンドを分けない」というのがあります。デバッグのためだけに新しい知識を要求するのは好きじゃないのです。

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-05-16 (土) 20:26:39 (130d)