* 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 になります。 --適当に検索して見つけた http://cute.sh/solairo/p_c/c_01.html にもそう書いてありました。 -当然ですがこの結果は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の基本的な考えとして、「デバッグ時と通常時でコマンドを分けない」というのがあります。デバッグのためだけに新しい知識を要求するのは好きじゃないのです。 * こめんと欄 #comment