nodule::operator deleteで参照残留

ZTシステム構成図7.ZELを起動してSIMPLEGRAPH:DecompConnectedComponentの冒頭でCOMPLISTリストのキャンセルを実行しているところでnodule::operator deleteが(nptr->refcnt && PHASE != CLEARTABLE)というエラーを出している このリストはCOMPLIST#23242で要素数40個,cancelの冒頭ではパラメータは整合している cancel動作そのものはDATALIST::cancellにゆだねているが,事後bottomlistとcurnodを参照解除している

削除されているのはREFLINK#23399で残留カウントは1,PHASEはSAMEGENEMARRIAGE(重婚同類グラフを生成).このオブジェクトはcancel対象リストの末尾ノードだ.参照リストを見ると空になっているので,参照カウントが残っていること自体誤りと思われる.これはどういうことだろう?⇒間違っているのは参照リスト管理の方だ.#23242COMPLIST[3]からの参照が残っている.先に参照リスト管理の方を見ておこう.REFLINK#23399が空になっている.その直前には,#23242COMPLIST[3]からの参照を処理していたのだが…

~NODULEでREFERISTスロットをクリーンアップしていた.参照/接続関係はノードの生死に関わりなく有効でなくてはならないから,参照リストもパーマネントオブジェクトでなくてはならない.⇒そもそも,定数定義でREFERISTとEXTRA_ANCHORの順位が逆転していた.よくこれで動いていたものだ…※ これで参照リスト管理の問題は解決した.

※これは不思議でも偶然でもない.このシステムでは(必ずしもすべてはないがほとんどの)スロットが仮想化されているためだ.少なくともアクセス関数によってしか読み書きされないスロットは原理的に仮想化されたスロットと言っても間違いではない.

curnodからの参照が残る問題は,LIST::dataCountDownの論理ミスだった.論理ミスというより,見落としというべきかもしれない.Sansyo(LISTsCURNOD, lower)でcurnodを次ノードに移動しているが,終端の場合は空になるので,toplistに付け替えていた.しかし,このタイミングではtoplistにはまだ現在の値が残っているため,Sansyo(LISTsCURNOD, toplist)が残ってしまう.間違いのない方法として,Sansyo(LISTsCURNOD, upper)で一つ前のノードを指すようにした.これで問題は解決し,エラーなしでアプリを起動して終了できるようになった.整理する前にバックアップを取っておこう.

アプリ終了時のnodule::resetNringでNODEREFLISTが大量に残留している.1296個もある.どうしたのだろう?そもそもこれらはどこで使われているのだろう?⇒分かった.完全参照リスト管理だ.参照リストが置かれているスロットは圏外(永続オブジェクト)として削除しないようにしたためだ.従来論理では~NODULEで参照リスト管理用スロットまで削除していたのを残すように変更している.フェーズがBEGINNINGS以下ではすべて削除するように修正した.現行ではCOUPLING::EraseFamilyTreeでBEGINNINGS状態に戻る.

リスト検査関数CheckAllListの対象リストをLISTからDATALISTの範囲に拡張して,エラーが出た.COUPLING::CloseFamilyBaseの入口で3つのNODEREFLISTでエラーが検出される.⇒checkdatacountは仮想関数でNODEREFLISTも固有関数を持っているが,checkdatacountの戻り値がerrcountを返すように仕様変更されているのに対応していなかった.⇒エラーを返すように修正した.

アプリ終了時CallSetCouplingPtrでCouplingを削除した後,ゴミ箱の中にはオブジェクトが8036個,Nリングに584個,合計8620個のオブジェクトが計数されている.Nリングに入っている584個のうち一つを除いてすべてNODEREFLISTだ.残る一つはゴミ箱そのもの.ゴミ箱の中には別に713個のNODEREFLISTが入っている.ゴミ箱を投棄するとNリングの中身まで空になってしまうのはなぜか?⇒NODEREFLIST(参照リスト)はゴミ箱に入っているオブジェクトに付帯しているため,ゴミを焼却処分したとき一緒に処理される.

EraseTreeViewはクローズファイルで実行されているが,ここでPHASEをBEGINNINGSではなくINITIALSTATEに留めるようにすると,1296個生成された参照リストはすべてNリングに移ってゴミ箱には一つも残らない状態になる.NODEREFLIST(参照リスト)はBEGINNINGSより上のフェーズでは(永続オブジェクトとして保護されているため)自動的に削除されないようになっているからだ.

人間(じんかん)到るところ静山あり

静山とは墳墓の地の意.これこそまさにCDD(制御された解体)が希求しているところだ.いつ,どこで削除されてもきちんと始末が付けられることがCDDの目標である.前に「構造物を解体するのは簡単だ」と述べた.構造物はグラフ理論的には木であり,終端の葉から削除することによって秩序正しく解体できる.実はゼルコバの木で採用しているデータ構造にはこのような骨格としての木が埋め込まれている.つまり,ゼルコバの木システムはCOUPLINGオブジェクトを頂点とする木を構成しており,すべてのオブジェクトはこの木の一部である.プログラム開発の進化という観点から見ると,我々のプログラムは少なくとも「脊椎動物」の段階に達したと言えるだろう.逆にこのような骨格を持たないシステムは「軟体動物」の段階に留まっていると言えるかもしれない.

我々のシステムが元々木構造を取っているのなら,解体はもっと簡単にできてもよいのではないか?確かにその通りだが,その基礎の上にさまざまな系統の経路(血流系,呼吸系,リンパ系,神経系,ホルモン系,消化系,排泄系,運動系,その他もろもろ)が多重ネットワークを構成しているためことはそう簡単にはゆかない.現行でもあるオブジェクトを削除するとそれに接続している下位オブジェクトはすべて自動的に削除されるようになっているから,システム全体を破棄するためには,頂点のCOUPLINGを削除するだけでことは済むようになっているし,実際,現行でも最終的にはCOUPLINGの削除で止めを刺すようにはなっている.従って,CDDが完成するということは,現在COUPLING削除の前段で行っているすべての前処理が不要になることであると考えられる.

アプリ終了→…EraseTreeViewでGENEBOXを削除して被参照カウントの残留が出た GENELIST#34→GENEBOX#15223が残っている

このエラーにはSWO(SearchWrongObject)で仕掛けている検査ルーチンの動作が関わっている.snum=34というオブジェクトを追いかけているのだが,そこで使っているLIST::top()という関数が副作用を持っているためだ.topはリストの先頭要素を返すルーチンだが,curnod(カレントリスト要素)を更新するような動作になっている.昨日はcurnod自体を廃止しようとしていたのだが,一部で使われているため(直ちには)廃止することができなかった.top()をtoplistに変えることはできると思うのでやってみよう.⇒うまくいった.

アプリ終了の出口近くで,残留したLIST派生クラスのインスタンス64個のうち1個でデータ数不一致が発生している #34GENELISTでデータ数13のところゼロになっている ⇒ toplistはすでに空になっているので,datacount 不整合と考えられる.この原因はGENELISTではまだCDDをサポートしていないためと考えられる.⇒対処した.

現在実装されているLISTの派生クラスにはNLISTとnlistがある.いずれもテンプレートクラスでその下には以下の派生クラスがある.

  1. GENELIST GENEBOX ◯
  2. PAIRLIST PAIRBOX ◯
  3. NODELIST SIMPLENODE 
  4. EDGELIST SIMPLEEDGE ◯
  5. TRIBELIST TRIBEBOX ◯
  6. COMPLIST REFLINK

最後のCOMPLISTはnlistの派生クラスで,それ以外はすべてNLISTだ.これらのうちの4つはすでに対応済みになっているので残り2つも片付けてしまおう.修正はただ単に各リスト要素クラスのデストラクタからdataCountDownを呼び出すというだけだから,あっと言う間に終わってしまう.~クラス名のデストラクタとその中から呼び出されるDisposeの切り分けはややあいまいだが,基本的にDisposeでは参照解決を行い,dataCountDownとCleaSlotだけはデストラクタで行うということにしておこう.デストラクタの入口では重複実行を回避するために if (finished()) return; としているが,これは完全に削除されたか,ないし~NODULEの処理が完了したことを意味しているので,重複実行される可能性はゼロではない.個別のデストラクタではこれと別に入口で disposing++;  を実行しているので,これも見ることにする.

いや,これだけでは手抜きかもしれない.安全のため,そのリストに自ノードが接続していることを確認してからdataCountDownすることにしよう.COMPLISTは少し厄介だ.このリストは何をリスト要素としているのだろう?なるほど,なぜCOMPLISTがnlistでNLISTではないのか?ということが分かった.リスト構造は接続によって構成されるが,接続するスロットがゼロスロット固定なので,複数のリストに同時に連結することはできない.NLISTはオブジェクトを直接接続して構成されるリストだが,nlistはリスト要素クラスオブジェクトという特殊オブジェクトを要素とし,そこから実際のオブジェクトを参照するようになっているはずだ.COMPLISTは以下のように定義されている.

class COMPLIST : public nlist<SIMPLENODE>

いや,ちょっと違うかもしれない.nlistの定義を見ると,

nlist 基本オブジェクトリスト(リスト要素はnoduleの派生クラスオブジェクト)

となっている.NLISTとnlistの違いはなんだろう?NLISTの説明もまったく同じだ.

NLIST 実装オブジェクトリストクラス(リスト要素はnoduleの派生クラスオブジェクト)

ただし,テンプレートが少し違う.

template <class NODECLASS>    —  NLIST
template <class LISTNODE, int CID>  — nlist

いや,それだけではない.nlistではSLOTZEROではなくSLOTONEを使っている.しかし,オブジェクトは参照でなく接続として生成されている.ただし,そのオブジェクトは<NODECLASS>で指定したオブジェクトではなく,REFLINKという特殊オブジェクトだ.REFLINKはサイズ1の配列クラスとして定義されている.上のSLOTONEというのはREFLINKからオブジェクトを参照するためのスロットだ.nlistの定義というより,説明文は間違っているか,少なくともあまり正確ではない.

nlist 基本オブジェクトリスト(リスト要素REFLINKからnoduleの派生クラスオブジェクトを参照)

としておこう.従って,COMPLISTではREFLINKからdataCountDownが送られることになる.⇒これで6つでそろった.これですべてだろうか?LISTから直接生成されているインスタンスはなかったろうか?ないという訳ではない.というか,存在する.たとえば,参照リストなどはそのタイプだ.系列枠のスプリット検定用リストなどというものもある.シンメトリ婚スプリット検定用リストというのもこのタイプだ.DATALISTは2つのタイプのリストを生成することができる.一つは前出したREFLINKをリスト要素とするもの,もう一つのタイプは一般オブジェクトクラスのnoduleを要素とするものだ.REFLINKではすでに整備されているが,noduleにも同様措置を取る必要がある.

noduleクラスにはリスト検索ルーチンとしてgetlistというのがある.これはノード型という属性をチェックして所属リストか否かを判定しているが,ここではこれらの属性を無視してより一般的なgetUpperClassNode(‘h’)で検索することにする.これで少なくともリスト要素に関しては万全整ったのではないかと思われる.

参照カウントの不一致が出てしまった.多分,~noduleで親リストを探すのに一般的手法を使ったためではないかと思われる.終末処理ではnoduleのデストラクタは最後に呼び出されるから,それ以前にすでに処理が済んでいるものを重ねて処理することになってしまっているのではないか?これを回避するためにはやはり,旧来ロジックのような属性チェックが必要ということだろう.⇒どうもまだ通っていない.

▲ZTシステム構成図7.ZELを起動してSIMPLEGRAPH:DecompConnectedComponentの冒頭でCOMPLISTリストのキャンセルを実行しているところでnodule::operator deleteが(nptr->refcnt && PHASE != CLEARTABLE)というエラーを出している このリストはCOMPLIST#143473で要素数40個,cancelの冒頭ではパラメータは整合している cancel動作そのものはDATALIST::cancellにゆだねているが,事後Bottomlist(NULL)を実行して参照解除している

多分これは以前はDATALIST::cancellの前に実行していたのではないかと思う.bottomlistやtoplistの健全性はつねに検査されているので,事前にセットするのはやはり好ましくない.この参照削除に責任があるのはリスト要素であるREFLINKそのものと考えられるが,REFLILNK:Disposeではなにもやっていない.dataCountDownをDisposeに移動し,合わせてLIST::ClearSansyoを実行するようにして解決した.ただし,dataCountDownではリストのパラメータの調整を行っているのだから,本来ならClearSansyoを実行しなくても動作するはずである.そうなっていないのは,LIST::dataCountDownに問題があるからだ.

実際の削除を実行しているのはDATALISTだが,LIST:dataCountDownではLISTクラスのメンバ変数についての操作しか実行していない.これでは明らかに不十分だ.現行ではdataCountDownという関数はLISTクラスに唯一存在し,それだけですべてをカバーするようになっている.もし,それを仕様とするのであれば,LIST:dataCountDownですべてのことをやらなくてはならない.しかし,それでは基本クラス以下が何をやっているかすべて知らなくてはならないので,現実的ではない.むしろ,LIST::dataCountDownの中から下位クラスに通知をパスして,いまの場合で言えばDATALIST:dataCountDownを実行するようにさせるべきだろう.そうしておかないと,今後さらに拡張したときに問題が生じるおそれがある.

実行順序に関して言えば,デストラクタの実行順序とは逆に下位クラス(基本クラス)のdataCountDownを先に実行した方がよいのではないかという気がする.⇒いや,素直にデストラクタの実行順序を踏んだ方が安全なのではないか?⇒それはそれでよいことにしよう.原則的にはすべてのクラスが固有の遺言執行書を書く必要がある.遺言執行書がこように階層化されるとすれば,この関数は仮想化すべきなのではないだろうか?さもないとどこかで中抜きされて下位クラスに渡されてしまう可能性がある.修正してみよう.

いや,そんな簡単な話ではないのではないか?noduleのデストラクタで遺言書を送信したときのことを考えた方がよい.noduleの生のインスタンスがリスト要素になる場合があるから,noduleには遺言書を発行する資格がある.しかし,たとえばSIMPLENODEの中にはnoduleが入っているからSIMPLENODEとnoduleが同時に遺言書を発行したら,処理がかち合ったり,ダブったりする可能性はないか?対象を直列リストに限定したとしても,リストクラスはそれ自体が階層になっているから,各クラスの処理にダブりがない限り,各クラス別に遺言書を出すことも合理化できるとしよう.リストクラスがNあるとすれば,N個の異なる遺言書のテンプレートがあることになる.これはいいだろう.

問題はこれを発行する側にある.つまり,オブジェクト自身クラス階層の上にあり,デストラクタはこの階層を下降して逐次処理を行うという仕掛けになっている.つまり,あるオブジェクトは玉ねぎのようにそれ自体階層化している.しかし,上位ですでに遺言書を発行していることはblackflag をチェックすれば確認できるはずだ.従って,noduleで起きた問題は検査を厳格にしなくても,フラグをチェックするだけで回避できた可能性がある.⇒まず,この点について確認してみよう.ロジックを元に戻してエラーが発生することは確認できた.⇒いや,このエラーももう少し詳しく見る必要がある.

エラーは参照リスト管理の中で起きている.この中ではリスト要素の削除が実行されるので,内部的に参照リンク操作が含まれている.リスト要素はnoduleなのでnoduleからCDCが発行され,~nodule→ find→ top→ SansyoでReferenceControlの不一致が発生している.⇒何とか強引に動かした.ポイントは「参照リスト管理」の中では参照を操作してはならないという点だ.いや,実際そういう作りにはなっているのだが,remove関数の中でデバッグ用に実行している検査関数が副作用を与える点が問題だ.top()やcurnodを存続させるとしても,検査用に別途副作用のないルーチンを確保しなくてはならない.

どうも先にそれをやらないとあちこちで躓いてしまうのではないか?引数を持たないnext()などはcurnodを基準としているので,それがないと都合の悪いことが起きる.最近は引数なしのnextを使った記憶はほとんどない…いや,それほどは使われていない.全体で5箇所だけだ.これは真っ先に廃止してよいだろう.Next(void)というのもある.curnodに関してはベタ参照にしてしまうという手もないわけではない.あまりよい考えではないかもしれないが… なぜだろう?nlistは引数付きのNextを持っていない.⇒Followingという名前を使っていた.

ついでにtopとTopも廃止したいところだが,その前にバックアップ.top()はさすがに多い.303箇所もある.curnodは50箇所の出現する.ただし,そのうちの10箇所は「curnodを廃止する」だ.⇒curnodの廃止はひとまず置いて,先にいま停止しているところをクリアしてしまおう.上記で解析した通り,この問題を解決するためにはDATALIST:dataCountDownを作るしかない.というかそれが最善策だと思う.方針を立ててみよう.

  1. 現在の「制御された解体」の範囲を直列リスト操作に限定する
  2. 各リストクラスは個別に事後処理関数dataCountDownを持つ
  3. ただし,削除そのものを実行していないクラスでは持たなくてもよい
  4. 削除対象となるリスト要素のデストラクタでは自ノードが接続するリストを検索し,存在すればdataCountDownを送信する
  5. 親リストとのコンタクトが切れている場合には送信しない(できない)
  6. また,自ノードのblackflagがオンの場合も送信しない(送付済み)
  7. dataCountDown関数では入口でblackflagをインクリメントする
  8. dataCountDown関数では出口で下位のdataCountDownを呼び出す
  9. 削除後の後処理としてdataCountDownを実行する
  10. ただし,対象リスト要素のblackflagがオンの場合には実行しない
  11. 処理完了後にblackflagをリセットしない

現時点ではLIST::dataCountDownしかないので,追加してインプリメントされるのはDATALIST::dataCountDownだけだ.この関数では自クラスが管理するtoplistの更新を行う.これは現在 delete を実行しているブロックのロジックをそっくりdataCountDown関数の中に移植するだけのはずだ.dataCountDown関数は内部操作の場合と外部で削除された場合の共通論理となる.

一つのクラスでdeleteを実行している箇所が複数ある場合にはどうなるか,それぞれに異なる遺言書を付けるのか?⇒そのようなことはあり得ない.もし,現行でそういう論理が存在するとすれば,deleteを実行しているブロックを関数化して一箇所にまとめるべきだ.そうできないという理由は考えられない.もちろん遺言執行書は一クラスで高々一つ持つだけだ.DATALISTにはdeleteを実行している箇所が3箇所あるので,deleteElementとしてまとめよう.LISTは1箇所だけなのでその必要はないが,同じ名前の関数を作って分離することにする.

制御された解体(controlled demolition)のポイントが大体つかめた

制御された解体(controlled demolition)のポイントが大体つかめた.病院のベッドであるいは自宅で近親者に囲まれて最後の時を迎えるのがノーマルな死であるとすれば,事故死や自殺,孤独死などはアノーマルな死と言える.このような意味でオブジェクトのアノーマルな削除に対処するというのが制御された解体の目的である.

削除されたオブジェクトはメモリから消去(ないしゴミ箱に移動)される前に必ずそのデストラクタが呼び出されるので,知人・関係者に死亡通知を送りつけることができる.その時点で知人・関係者とのコンタクトが取れなくなっている場合には何もできないが,知人・関係者がすでに他界しているのであれば(できないことは)問題にはならない.

関係者側では死亡通知を受理した時点で事後処理を実施する.この方式なら関係者が関わりを持たない場所でアノーマリィに削除された場合にも対処することができるので,システムの一貫性・データの整合性を保つことがこれまでよりはるかに容易なものになる.PAIRLIST→ PAIRBOXとEDGELIST→ SIMPLEEDGEの関係に限定してテスト的に実装しているが,まだエラーが残っている.

アプリ終了でEraseTreeView→~TRIBEBOX→… SIMPLEGRAPH:TakeRemainSansyoを実行中にLIST::nextでエラーが検出される

TakeRemainSansyoのループで最初にNODELISTを処理した時点でデータ不整合が発生している.対象グラフはTOPOLOGYのgraph3(系列木グラフ生成用枝グラフ)だ.識別し易いようにgraph3をリネームしてTribeTreeGraphとしておこう.このグラフの節点は系列枠だ.⇒確かにNODELIST::Removeを実行しただけでEDGELISTに影響が及んでいる.不思議だ…節点が削除されるとそれを参照している枝にも影響が出るということのようだが… EDGELISTはNODELIST要素を参照しているので,その参照がクリアされることは確かだが… NODELIST::Removeが実行されると,EDGELISTの要素は23個から16個に減少する.つまり,7個削減されている.事後のdatacountとtoplistの値は正しいが,bottomlistの値がtoplistと同じになっている.

グラフの枝の両端点のいずれかが削除されるとその枝も削除されるようになっているが,どこでやっているのかはよくわからない.いずれにしても,この操作で不整合が発生していることは間違いない.節点の削除を実施するグラフの関数は2つある.releaseとReleaseだ.前者は節点が参照するオブジェクトが削除されたとき,後者は節点そのものが削除されたときの操作だが,いずれも枝の両端点のうちいずれかが削除されたときには,枝を枝リストから削除するようになっている.しかし,枝の削除を実行するSIMPLEGRAPH::removeでSIMPLEEDGE::removeの前後をチェックしているが,不整合は検知されていない.

TakeRemainSansyoの入口でトリガーを掛けても,SIMPLEGRAPH:remove(SIMPLEEDGE*)実行では停止しない.⇒TakeRemainSansyoは再帰的に実行されている!再帰から戻ったときに値が戻ってしまうのではないか?というか,いまのblackflagのプロトコルは再帰実行されることを予定していない.また難問が出てきた… 現行方式では delete → Dispose → dataCountDown → 事後処理のフローは直列かつ即時・排他的に実行されることを予定している.blackflagを関数のローカル変数に置くことができれば再帰実行に対処することは難しくないが,delete の実行と dataCountDown は独立の別関数でまったく非同期に実行されるため,クラスのメンバ変数とするしかない.つまり,現行方式ではこの問題には原理的に対処できない.

ではどうすればよいか?こうなれば豚のトレーサビリティシステムのように個体に識別子を付けて履歴管理するしかない.いや,個体識別子はもちろん付いている.豚の全頭履歴管理を行うというのは確かにすごいと思うが,プログラム的には比較的単純なデータベースがあれば済む.多重な関係ネットワークを持つオブジェクトの終末管理にはあまり向いていない.いずれにしても個体にマークを付けるしかないことは明らかだ.マーク名はblakflagとして,それをどこに置けばよいか?拡張性を考えれば,noduleクラスメンバとするのが適切だろう.noduleは基底クラスNODULEから直接派生する唯一のクラスであり,オブジェクト全数管理のためのNリングシステムとリサイクルシステムおよびUNDOシステムを備えた応用システム構築用の基本クラスだ.オプションではあるが,完全参照リスト管理も持っている.これに終末期管理のための制御された解体機能が備われば,ほぼ万全と言えるのではないか?

死亡通知同期用フラグとしてnodule::blackflagを設置すれば,dataCountDownによる死亡通知は不要になるのだろうか?いや,ならない.養豚場から盗まれた豚がどこかで密殺された場合,これがなければ終末処理ができない.今日はまだ修正は1件も入っていないので,このまま続けよう.⇒フラグではなく,deleteの実行者のリンクを直接書き込んでおくというのはどうだろう?こうすれば,いちいちデスノートの宛て先を探しまくる必要はなくなる.そのアドレス宛てにデスノートを送るだけでよい.デスノートは定型文なのでそれぞれのクラスで用意する.DeathNoteという仮想関数を一つnoduleに作っておいてもよいのではないか?nodule::nodule *executerとしてみよう.この変数はオブジェクトのライフタイムで一度しか使われないのだから,operator new で初期化しておけばよいだろう.

いや,少し違うのではないか?dataCountDownは密殺された豚の死亡通知を管理者に届ける仕組みだ.dataCountDownを管理者に送れるということは管理者とのコンタクトが切れていないという証明であり,管理者がexecuterに署名したところで何の意味もない.密殺者は署名できないし,適切な終末処理を持っていないのだから,署名しても意味がない.やはり,blackflagを使うしかないと思う.

blackflagとexecuterを併用することは可能だが,そこまでする必要もないだろう.まだ障害が残っている.

今度はTRIBELISTのCleanSansyo中にbottomlistに関するエラーが出た LIST::nextで検出されている.TRIBELIST自体の不調だ ⇒ CleanSansyoの入口ですでに不正規になっている.これを防止するには,TRIBELIST自体にCDD(Controlled Demolition)を導入するしかない.⇒ダメだ.効果がない.⇒dataCountDown自身でエラーを作っている.⇒対処した.

▲COUPLING::CloseFamilyBaseが過剰に呼び出されている ファイルをオープンしていないときはNOPリターンでよい ⇒ 未了

▲COUPLING::EraseFamilyTreeにモードを設け,アプリ終了時には「EraseFamilyTreeで削除しない@20201120」で保留したブロックを強制削除する ⇒ 保留

▲アプリ終了の出口近くで,残留したLIST派生クラスのインスタンス64個のうち1個でデータ数不一致が発生している #34GENELISTでデータ数13のところゼロになっている

GENELIST#34を追跡するようなSWO(SearchWrongObject)を仕掛けて,完全参照リスト管理から「参照元ノードリスト不記載」エラーが出た ⇒ SWOがクリティカルゾーンに入って再帰が起きているためだ.escapeで回避するようにして止まった.

▲リストアクセス関数のLIST::topなどを実行するとcurnodへの書き込みが発生する topやnextなどのルーチンは副作用がないことを予定しているので,このような動作は望ましくない 実際問題としてcurnodはほとんどまったく使われていないと考えられるので廃止してよいのではないか? ⇒ 一度バックアップを取ってから廃止手続きを進めることにしよう.LIST::insertはcurnodを参照する動作になっている.かなり疑問の多い論理なので後で精査することにする.

probeではアイドリング時のカウントアップを避けるため,DRAWSTAGEフェーズではScoutとscountを更新しないようにした ⇒ これはかなり重要なポイントだ.これでようやく,トラッキングの完全再現性が保証されるようになった.

▲アプリ終了→…EraseTreeViewでGENEBOXを削除して被参照カウントの残留が出た GENELIST#34→GENEBOX#15223が残っている

死者からのコールバック

まだ,制御された解体(controlled demolition)の実験段階だが,反応は悪くない.アプリ終了時にTREEVIEW::EraseTreeViewで描画オブジェクトの無差別削除が実行され,PAIRBOXのデストラクタが呼び出されると,PAIRBOXでは自ノードの所属するPAIRLISTを割り出して(もし,存在すれば)PAIRLIST::dataCountDownを呼び出す.これは自分自身の死亡通知を管理者に送るのと同義で,管理者はそれに従って(埋葬などの)事後処理を実施することができる.

これまでもCleanSansyoなど同等趣旨の関数は実装されてきているが,今回はっきりと,これが手続き(処理)ではなくコミュニケーションの一種であるということに気付いた.デストラクタからの呼び出しは謂わば死者からのコールバックに相当する.自分ですべてをやろうとするより,それを知っている人に依頼した方が早いし,確実だ.自分の葬儀を生前にデザインすることは可能だが,実施することはできない.つまり,必要なのはプロシージャからプロトコルへの転換だ.

ZTシステム構成図7.ZELを基準ノード=202 NODULEで開いて終了するとき,PAIRLIST::dataCountDownでリスト上にオブジェクトが見つからないというエラーが発生する

dataCountDownの呼び出しはPAIRBOXのデストラクタ→Disposeから実行される.ノード対リストを管理しているPAIRLISTを割り出すためにはPAIRBOXがPAIRLISTに接続していなくてはならない.ということは,このノードはリスト中に含まれているのにfind関数で見つけられなかったということになる.⇒PAIRLIST::findは直列リストの部分しか見ていない.PAIRLISTはそれから分岐した「端点共有ノード対リスト」を持っているので,そちらも探さなくてはならない.もし,findという関数の仕様が元々「直列リスト要素を探す」ことであるのなら,別の探索関数が必要になる.⇒findは以下から呼び出されている.

  1. CheckShiftedPairBox(bool force) (TOPOLOGY)
  2. RetrieveGhost(void) (NAMEBOX)
  3. dataCountDown(PAIRBOX * pbox) (PAIRLIST)
  4. putbottom(PAIRBOX * pbox, PAIRBOX * common) (PAIRLIST)

いずれもfindの動作を拡張しても問題ないように思われるので修正しみよう.⇒いや,現行でもPBOX::findは端点共有リストまで探索するようになっている.PAIRLIST::nextでそれをやっている.⇒いや,それどころではなくなった.とんでもないことが起きている.nodule:grandparentの実行カウントがテストのたびに変動している.あり得ない!これは決定性チューリングマシンだ.仮にマルチスレッドで並列実行されるようなことがあったとしても,止める場所は同じなのだから実行回数が変動するはずがない.

nodule::grandparentの引数オブジェクトのsnumが225749のときダンプするように仕掛けて変動を見たところ,画面が出力されるまでは一定だが,終了ボックスをチェックして最初にダンプされる時点でカウント40くらいの変動がある.信じ難い現象だ.何が影響しているのだろう?画面が出てから最初のダンプまでの間にgrandparentは3万回近く実行されている.ccid=’&’ COUPLINGでダンプしてみよう.どうもこの関数は画面を表示してアイドリングしている間にも呼び出しが掛かってくるようだ.⇒カーソルが系図画面に入っただけで呼び出しが掛かってくる.

getUpperClassNodeというgrandparentを呼び出しているだけの関数があるので使ってみよう.getUpperClassNodeは画面を開くまでに37058回呼び出されているが,アイドリング状態では変動しない.OCXではOnMouseMoveを取っているので,ここからDLLの関数が呼び出されているのだろう.どんな関数が呼び出されているのか知りたいが,また後でということにして,PAIRLIST::findに戻ろう.

PAIRPLIST::DumpPairListでもこのノードはダンプされない.PAIRLIST:nextboxが間違っている.というか,bottomに間違った値が入っている.nextboxは現在要素がbottomのときは次要素としてNULLを返している.bottomは誤っている可能性が高いので生値を返した方がよい.いや,この関数ではなくLIST::nextだ.⇒修正しておこう.⇒LIST::nextの中でbottomの整合性チェックを行うようにしたところ,この不正がかなり早期に発生していることが検出された.

MakePairListClean…→ RetrieveGhost…→ PAIRBOX::Dispose→ PAIRLIST::CleanSansyoで起きているようだ.PAIRBOXはLIST::_removeで削除されている.⇒いや,これは過渡的なものではないか?⇒いや,違うかもしれない.bottomlistの操作をdeleteの後に実行するようにしたが,同じだ.LIST::_removeの作動中フラグOnLISTremoveを立てて,この間はチェックしないようにした.

この後,bottomlist, topolistの不整合が続いたので,LIST::_removeで行っているパラメータ更新をすべてLIST::_removeに移動した.ただし,これは例外的な措置でリスト要素がPAIRBOXである場合に限定される.これでPAIRBOXの削除に関わる不整合は完全に一掃された.

TREEVIEW::EraseTreeView→~TRIBEBOX→…SIMPLEGRAPH:TakeRemainSansyo→…LIST::nextでbottomlist不整合が発生した

これは全く上記と同じ事象と考えられるので,対策も同じだ.EDGELISTはNLISTの派生クラスなのでLISTの孫クラスに当たる.SIMPLEEDGEのデストラクタでdataCountDownを発行し,それをLISTで受けるようにすればよいはずだ.NLISTは追加パラメータを持たないので,すべてLISTで処理できる.いまのところdataCountDownはPAIRLISTのメンバ関数になっているが,これをLISTに移管してしまった方がよい.まず,それをやっておこう.⇒対処した.

SIMPLEEDGE::Disposeでは始点・終点オブジェクトへの参照解除しかやっていない.getgraphという関数はあるが,所属リストの取り出し関数もない.ここでは簡略にDisposeの中で直接LISTを探して,そこにdataCountDownを送ることにしよう.ただし,getUpperClassNodeではそのオブジェクトに埋め込まれたCIDを探しにゆくので,LISTを直接探しても見つからない可能性がある.というより,見つからないだろう.pclassid(基本クラスIDテーブル)を使って親クラスを探すことができたはずだ.NODULE::IsSuperClassという関数がある.これを使ってgetUpperClassNodeを拡張すればよい.⇒誤動作する可能性はないだろうか?可能性はゼロではないが,まずないと考えてよいのでは?

求めるクラスIDをCIDとしたとき,CIDをそのものずばり持っているオブジェクトがあればそれを優先しなくてはならない.その手前にスーパークラスにCIDをもつ別のクラスオブジェクトがあった場合には問題になるかもしれない…安全策としては,まず現行方式で探索し,発見できなかった場合には拡張探索するというのが順当なのではないだろうか?とりあえず,そのように実装してみよう.拡張探索には現行探索も含まれているので多少冗長にはなるが,仕方ない.⇒実装した.

アプリを起動して,TRIBELIST::TribeRelocationのステージ【7.2】完全木検定:すべての系列を完全系列として正準化するでMakePairList…→ TOPOLOGY::CheckPairList… →GENEBOX:CheckInverseCycle…→ LIST::cancel…→ ~SIMPLEEDGE→ Dispose→ LIST:dataCountDownでdetacountが負になった.CheckInverseCycleでグラフを初期化するためにEDGELISTをキャンセルしているところだ.LIST→NLIST→EDGELISTの間のどこかでdatacountをデクリメントしているのだろう.

いや,違う.LIST::cancelではDATALIST::cancelが実行される.削除は自前でやっているが,cancelは下請けに出している.LISTからDATALISTに通知を送るか?ないし,DATALISTを含む大改造に着手するか?⇒これはかなり難しい問題だ.

  1. リスト要素のデストラクタでは自分の死因を割り出すことはできない
  2. dataCountDownはリスト要素の属するLISTの派生クラスのインスタンスに送られる これはXLISTとする
  3. XLIST→LISTではリスト要素のクラスによってパラメータの更新を実施
  4. 処理がLISTの内部で完結している場合はこれでよいが,基本クラスのDATALISTに処理を依頼している場合には,DATALISTに死亡通知を送らなくてはならない
  5. DATALISTではdataCountDownを受け取ったときにはパラメータ更新を実施しない

いや,もう少し簡単な方法がある.DATALISTに何かフラグを設ければよい.たとえば,blackflagとしておこう.LISTではdataCountDownを受け取ったときにはBFを立てればよい.DATALISTではBFが立っているときにはパラメータの更新を実施しないでフラグを落とすだけとする.LISTでたとえば_removeなどの処理を行った場合には_removeで落とせばよい.これでよいのではないだろうか?

リスト要素をdeleteする前にフラグをリセットしておけば,まず間違いはないと思われる.外部で削除された場合にはどうなるか?LISTではDCDを受け取ったタイミングでBF+するが,これをリセットするものはいない.従って,deleteを実行する前のリセットは必須である.

現行のdataCountDownにはまだ不備がある.この関数ではdatacount,bottomlist, curnodを更新しているが,toplilstは更新されていない.本来なら,datacountとtoplistの更新はDATALISTで実施しなくてはならないところだ.ただし,toplistはリスト先頭ノードの接続スロットなので,特に何もしなくても更新された状態にはなっている.つまり,現状でも問題はない.⇒修正は入れ終わったが,エラーが出ている.

▲アプリ終了でEraseTreeView→~TRIBEBOX→… SIMPLEGRAPH:TakeRemainSansyoを実行中にLIST::nextでエラーが検出される.

「終末期をどう生きるか」ないし「老化とはそもそもなんであるのか」

「ところで,世界の有限性が明らかになるなかで生きるとは,つまり限界超過生存(オーバーシュート)の状態で人類が生きるとは,ある意味で,人類全体が巨大な終末医療のホスピスに入るということではないだろうか?」と故加藤典洋が書いたのは今から7年前の2013年だ.これに全面的に同意するものではないが,2020年現在の世界をかなり正確に予見するものであったことは間違いない.限界超過生存(オーバーシュート)つまり,「生き過ぎてしまった」というのは個人的にも当てはまるところだが,「終末期をどう生きるか」ないし「老化とはそもそもなんであるのか」ということは問われなくてはならない.

ある存在が個体であるということは有限であるということであり,有限であるということは空間的にも時間的にも有限であることを意味するとすれば,個体である人間がいつか死に直面することは不可避である.生きていることには悲しみもあるが楽しみもある.いつか人はその楽しみを失うことになるのではあるが,そのプロセスを可能な限り苦痛の少ないものにすることが「老化」の本質であり,目標なのではないか?つまり,解体プロセスをできるだけ穏やかに進行させることが創造者の本意だったのではないか?そこには相当の創意・工夫も感じられるが,創造者の力を持ってしてもそれをパーフェクトなものにすることはできなかった.つまり,組み立てることは簡単でもそれを秩序正しく分解することはとても難しい.いま,我々が直面しているのはその問題である.

たとえば,ここに家と家を結ぶ入り組んだ街路を持つ一つの城塞都市があったとする.あるときこの街の市長が街の建て替えを思い付き,全住民に移転を命じた.移転して空家になった家屋は取り壊され,その家に接続する路はすべて封鎖される.ただし,この都市の路はすべて一方通行であるため,路の入口側で「この先行き止まり」の看板を立てなくてはならない※.しかし,一部の路は生活に必須な経路となっているため,封鎖ではなく迂回のための繋ぎ替えが必要になる場合もある.ある家から発する一方通行路の出口にある家を知ることはできるが,その家に達する一方通行路の入口の家を知ることはできない.移転が完了するまで残った住民すべてがこれまで通りの生活を維持できることがこの大規模工事の条件である.この工事を安全に施工するための指針を示せ.

※一方通行路の両端(入口と出口)を除き,この路に接する家はないものとする.また路同士は交叉しない(交差点は存在しない).

「地図があればいいんじゃないの?」と言われるかもしれないが,この街そのものを1個の地図と考えれば,徒歩で実地調査するのと地図を読むのでは計算量理論(計算複雑性理論)的には何の違いもない.生活に必須な路にはさまざまな種類がありそれぞれに担当の係がいる.万一「この先行き止まり」の看板が出ていない行き止まり路が発見された場合にはその工事を担当した係のクビが飛ぶことになっているが,手抜きや情報漏れ,資源不足などいろいろな事情から完璧を期待するのは難しい.(構造物を解体するのはこれに比較するとはるかに簡単だ.一番外側/内側から1個づつコンポーネントを取り外してゆけばよい)我々がやろうとしているのはこのようなネットワークの秩序だった解体,ある種の制御された解体(controlled demolition)に他ならない.

MakeAbsoluteを実行後にTOPOLOGY::CheckAtypicalMarriageが実行されている この関数は結婚連結線の所属を決めるために常用世代番号を使って基本世代枠リストにアクセスしている

CheckAtypicalMarriageの計算では絶対座標系を使っているのでMakeAbsoluteより前に移動することはできない.物理世代番号を使うように書き改める必要がある.⇒実際,nbox->getGeneration() – MinGeneration + 1で物理世代番号に変換している.⇒対処した.

かなり厄介な話になってきた.⇒いや,以外に容易に片付いた.少なくとも描画までは進める.ただし,出口検査は水平スプリット検査を除いてすべて絶対座標変換の前に移動した.また,水平スプリット検査はなぜか相対座標系では誤動作してしまう.元々絶対座標系で動作するように作られているのかもしれない…

絶対座標変換フェーズでGEN2DEVを実行して系列枠不在が多発している 系列枠はgroupから取り出しているので世代操作とは無関係のはずだが…これは,MAKEABSOLUTEフェーズ以外では発生していない.おそらく,これまでも発生していたはずだが,世代番号の取り出しを実施していなかったため発覚しなかったものと思われる.不要ノードと考えられるので,どこかのタイミングでパージした方がよい.⇒結婚枠と結婚リンクは一体とみなされている.また人名リンクと人名枠(0)もつねに不離不即とされるため,パージすることはできない.

絶対座標変換から外してもよいのではないか?実際,計算しようがない.ただし,HIDE_EXPOSEという状態もある.HIDE_EXPOSEというのは初期状態ですべての描画要素が隠蔽リスト上にあるという状態を示すものと思われる.現行ではHIDE_EXPOSEはゼロという値を割り当てられているので,識別が難しい.⇒隠蔽リスト上のノードは絶対座標系変換から外すというのでよいと思う.

以下の関数はCLEARTABLEフェーズ以下(アモルファス状態)では無動作で抜けるようにした.これらは比較的規模の大きな関数で理論的には不可能ではないとしても,いま直ちに対処するのは難しい.

  1. NAMEBOX::RetrieveGhost
  2. MARGBOX::cancelBetweenTwo

PAIRLIST::cancelでデータカウント残が出てしまった.PAIRBOXのデストラクタで所属するPAIRLISTを割り出してdatacountを削減することは不可能ではないが,PAIRLIST自身で実施するデクリメントとかち合ってしまう可能性がある.⇒方法はいくつか考えられる.①削除中のノードにマークを付けておく,②削除中のノードの控えを取っておく.いずれにしても,PAIRBOX::DisposeからPAIRLIST::CleanSansyoでコールバックされるので,削除中であれば本体でデクリメント,そうでなければCleanSansyoでデクリメントする.⇒いや,単純につねにCleanSansyoでデクリメントでよいのではないだろうか?

PAIRLISTはremoveとRemoveという2つの削除手段を持っているのになぜあえてdeleteで直接削除しているのだろう?removeはRemoveを引数TRUEで呼び出しているだけだが,Removeはやや複雑だ.ノード対リストは端点共有ノード対接続チェーンというのを持っている.また,この関数ではdeleteする場合とnodl_floatでフロート状態にしたまま切断する場合がある.後者は別のノード対リストに繋ぎ替えするための過渡的な状態だ.cancelで直列リストのノードだけを(端点共有ノード対チェーンに接続するノード対を無視して)deleteしているのは,それによって枝分かれのリストも同時に削除されるためだろう.一度バックアップを取ってから修正に入ることにしよう.

PAIRLISTはCleanSansyoをLISTに引き渡し,LISTはそれをDATALISTにパスしているが,DATALISTでは何もしていない.PAIRLISTはリスト上の他のノードにもCleanSansyoを要請しているが,リストというのは本来接続関係なのでほとんど空動作になっているものと思われる.datacountはDATALISTで定義されているものなので,ここで直接管理すべきものではないだろうか?カウントダウンする専用のコールバック関数を作るというのが一番確実であるような気がするのだが…

ただし,それをやるとすると,DATALISTのすべての派生クラスでdatacount操作の修正が必要になる.PAIRLISTはDATALISTのremoveなどの関数を使っていないので※,実験的にまず,ここで試してみるのがよいのではないか?フローティングする場合は別としてdeleteの場合は必ず削除されたオブジェクトからdataCountDownの通知を待つようにすればよいのではないか?⇒実装した.まだ上がっていないが,結構おもしろくなってきた.※⇒使っている.

少し先を急ぎ過ぎているのではないか?

どうも少し先を急ぎ過ぎているのではないかという気がする.危ない橋を渡っているのではないか?昨日は一度もバックアップを取らなかった.それだけ夢中になっていたのかもしれないが,壊滅的な状況になっている.昨日の修正を一度捨てて前日のバックアップまで戻るか?このまま泥沼の中を匍匐前進するか?状況はかなり悪いが,修正を少し戻して状況を観察してみることにしよう.もし,どうしてもダメなら退却するしかない.その前にいまの不良が前日版(ZELKOVA 2020-11-20)ですでに起きていたものかどうかを確認しておこう.

現在の環境設定は「_DEBUGマクロ未定義」と「FORMALVERSION」いずれもOFFのDebugモードだ.「完全被参照リスト管理を実行する」はONになっている.⇒いや,参照リスト管理そのもので障害が起きているので,これは止めておこう.⇒やはり元凶は昨日のnodl_floatの修正だ.これを戻せば取り敢えず動作する.明らかに何か読み損なっていたに違いない.ここでは現行論理は正しく現行仕様を反映していると考えるしかない.つまり,間違えていたのはこのわたしだ.

一番腑に落ちないのは,なぜこの修正が参照リスト管理にまで影響を及ぼすのか?という点だ.nodl_floatでは参照リスト管理に関わるスロットREFERISTはまったく操作していない…いずれにしても修正が間違っていたことに変わりはないのでここでは撤退以外の選択はない.一応この版はそれなりに動作しているので安定版として保全しておいた方がよいのではないか?もう少し整理を進めると整った状態になるとは思われるが,その前にまたドツボにハマる可能性もある.一旦休止して,仮修正をフィックスしておこう.いや,これは安定版と呼ぶにはほど遠い.画面はすでに表示されているが,停止している.非参照カウントの残留などが無数に出てくる.⇒この障害は前日版でも起きている.

▲ZTシステム構成図7.ZELを基準ノード=202 NODULEで開いて終了のとき,被参照カウントの残留と残留参照元を検出が多発する 

NAMEBOX #1828への参照として,#1819NAMEBOX[22]と#15158GENEBOX[18]が残っている.NAMEBOX[22]はNAMEsSAMEGENE:samegene 同一世代人名枠チェーン次ノード人名枠への参照,GENEBOX[18]はGENEBOXsBOTTOM:bottom1  同一世代人名枠チェーン末尾人名枠への参照だ.おそらくこのノードはsamegeneチェーンの末尾ノードなのだろう.この世代枠が特定できて,かつこのチェーンが壊れていなければチェーンをたぐることで始末することができる.この世代枠はどこが管理しているのだろう?

系列枠だろうか?⇒このノードのsamegeneはすでに空になっている.この障害はTREEVIEW::EraseTreeViewで起きている.ここでは描画オブジェクトの草刈りをやっているので細かいオペラ―ションはまったく無視されているはずだ.しかし,個別ノードがデストラクタで正しく対応していれば,それ(秩序だった解体)も不可能ではないはずなのだが… 少なくともNAMEBOX::Disposeではそれらしきことをやっていないので,まずそれを組み込んでみることにする.

samegeneは処理されず,デストラクタ出口のCleanSlotでクリアされている.NAMEBOX::CleanSansyoにはsamegeneをクリアしているところはあるが,チェーン管理はしていない.GENEBOX::CleanSansyoでは始末を付けている.同一世代人名枠チェーンは基本世代枠リストが管理している.baselistはTOPOLOGYの所有でTRIBELISTが管理している.TRIBELIST::CleanSansyoは実行されてはいるが,NAMEBOXに関しては何もやっていない.いや,やっている.実際この中でGENEBOX:CleanSansyoが実行されている.

このノード#1828が関係する世代枠は#15158だが,GENEBOX:CleanSansyoでは人名枠の世代から割り出した#15182が検査対象となり,ヒットしないため素通りになっている.⇒いや,この処理はDisposeの中でも実行されている.getSameGeneBoxで世代枠を取り出している.どこかで世代枠の付け替えが発生しているように思われる.問題ノードのデストラクタではすでにsamegeneは空になっている.どこかで強制的に参照解除してしまったのだろう.これは参照クリアをやっているところでチェーンを維持していないために起こっているものと思われる.少なくともGENEBOXは要素の追加と削除手続きを持たなくてはならない.GENELISTにはAddSameChainという関数がある.RemoveSameChainを追加しておこう.

どうも世代関係で間違えているようだ.世代枠リストをダンプしようとすると,GENEBOX::getLocationでエラーが発生する.いや,これは検査のロジック的間違いだ.というか,TRIBEBOX::getPotentialが間違っているのではないか?始系列の場合はつねに0を返しているが,どう考えてもこれはおかしい.また,KeitoMinとPotentialはつねに同じ絶対値を持つことになっているのでどちらかに統一してもよいのではないか?以下のように定義されているのだが…

Potential 基準ノードの物理世代番号,potential 優先ノードの常用世代番号=優先ノードと基準ノードの世代差

Potentialは(系統内の)すべての系列で共通なのだから,同じ値が返されなくてはおかしい.TRIBEBOX::getPotentialで「場合分けしない@20180912」というコメントが付いているのは,このことではないだろうか?もしかすると仮修正をフィックスする段階で誤って逆の論理を残してしまった可能性もある.しかし,ここをいじると,動かなくなる.⇒暫定的にGENEBOX::getLocationの検査ブロックだけを外して走らせるようにした.これで見ると,GENEBOX#15158の常用世代番号は-6ということになる.NAMEBOX#1828の世代は-4とされるので,合っていない.どちらが悪いのか?#1828はCARDTABLE(0) だ.

どうも訳が分からなくなってきた.基本世代枠リストには13世代分の世代枠が登録されているが,実際の図面では9世代しかない.基準ノードの位置は物理世代番号では4に当たる.この意味でCARDTABLE(0)の世代が-4というのは間違っていない.物理世代番号で0~3までの4世代のGENEBOXは空,最下層の12世代も同世代人名枠ゼロになっている.描画上の問題は起きていないので,このような状態でも描画は可能であるようだが,どこで調整しているのだろう?人名枠のgetGenerationは正しい値を返しているようなので,世代枠の世代取得関数が間違っているかないし古いのではないかと思われる.

image

基本世代枠にはノード対リストがリンクされているはずなので,内容をチェックしてみよう.ノード対リストの世代番号もダンプされる世代枠の番号とまったく同じだ.というか,多分ノード対リストは所属する世代枠から世代番号を取得しているのではないかと思う.中身が空の世代枠が存在することはやむを得ないとしても,同世代人名枠の人数と画面に表示された人名枠の個数が合っていない.

同世代人名枠数M≧表示されているカード数Nであるとすると,まず,Mがゼロの世代が5つ,ゼロでない世代が8に対し,実際の画面では9世代にカードが表示されている.カード数を上から拾うと,

N=4, 4, 15, 21, 24, 11, 6, 6 となる.一方ダンプでは,ゼロの世代を除いてM=2, 3, 9, 20, 15, 9, 5, 4 のようになってまったく一致しない,というか,表示されているカードより同世代人名数の方が少ないというのが理解できない.画面上に表示されているカードは重複を除いて80だ.Mの合計は67,Nの合計は91.多重が11あるのでそれを引くと80となり,表示されているカード数と一致する.ただし,コンソールには多重14という数字が出ている.この数字にも疑問がある.画面上のカードに世代番号を表示してみよう.

基準ノードの世代番号は-2,物理世代番号は4と表示されている.CARDTABLE(0)では世代番号-6,物理世代番号0でこれらの値は(それなりに)正しい.つまり,getLocationは画面上の物理世代番号を返し,getGenerationは基本世代枠リストと一致する(常用)世代番号を返している.どこで不一致が生じているのか?基本的には動作しているので,一部の世代関数のバージョンが古いことが考えられる…

原因は分かった.系列枠がすでにパージされてしまっているからだ.TREEVIEW::EraseTreeViewは描画要素を根こそぎ刈り倒して更地にしてしまうので,系列枠が存続していることを期待できない.系列枠が存在しないと基準ノードと系列優先ノードの世代差(ポテンシャル)が分からなくなってしまうためだ.物理世代番号を使うことはできないのだろうか?getLocationは最初にgetGenerationで常用世代番号を取得したあと,GEN2DEVで物理世代番号に変換しているだけだ.

すでに「解体モード」に入っているため秩序正しく分解してゆくのが難しいことは分かる.しかし,ここまで来た以上そこまでやってしまいたい…常用世代番号ないし物理世代番号をどこかに格納しておけば,それを取り出すだけになる.Bobject::COORDINATEには2という値が入っている.ABSOLUTEが1でRELATIVEが2だ.TREEVIEW:EraseTreeViewの冒頭,草刈りの前に値を切り替えている.

これを遅延させて,更地になってから切り替えるようにすることはできるだろう.絶対座標系への転換のタイミングですべての世代計算を「絶対世代番号」に切り替えればよいのではないだろうか?それしか方法はないと思う.Bobjectクラスは描画要素の一般クラスなので世代関係のパラメータは一切持っていない.世代が関係するのはNAMEBOX, MARGBOX, GENEBOX, PAIRBOXなどだが,すべてBobjectの派生クラスなのでBobjectにgenerationというメンバーを追加してやるのが早そうだ.それしかないのではないだろうか?BobjectにGetGenerationという仮想関数を追加し,変換時にはこれを使って各クラスオブジェクトの値を取り出したあと,絶対座標系に切り替えればよい.

絶対座標系に切り替わったあとは,個別の世代関数はGetGenerationを呼び出して格納した値を取り出すという段取りになる.つまり,GetGenerationという関数は座標系モードに従って,値の取り出し方を切り替えるという関数だ.一度バックアップを取ってから始めることにしよう.修正はそれほど大掛かりなものにはならないと思う.絶対世代番号という用語はすでに使われている.なんと呼べばよいか?暫定的に確定世代番号としてみよう.GetGenerationの設置を義務付けるためにBobjectでは純粋仮想関数としておくことにする.⇒常用世代番号を使うつもりだったが,一つ問題が出てきた.

PAIRBOXは物理世代番号しか使っていない.物理世代番号に切り替えるのは難しくないが,ほとんどの場面では常用世代番号を使っている.これをすべて物理世代番号に切り替えるというのもかなり大変だ.物理世代番号 = 常用世代番号+系統ポテンシャル という関係なのでもう一つ系統ポテンシャルという値が必要になる.系統ポテンシャルは系統ごとに異なるので一つだけ保持するという訳にはゆかない.⇒物理世代番号というのがもっとも合理的かつおそらくそれしかないと思われるが,書き換えが大変だ.ともかくそれをやってみよう.

仮想関数GetGenerationをBobjectのすべての派生クラスに配置するまでは簡単にできた.物理世代番号の取得はBobject::setabsoluteに次の1行を追加するだけだ.

_generation = GetGeneration(); // 物理世代番号を取得して保存

座標系の切り替えは以下のポイントで実施している.

  1. TREEVIEW::InitTreeView → RELATIVE
  2. COUPLING::EraseFamilyTree → RELATIVE
  3. TREEVIEW::EraseTreeView → RELATIVE
  4. COUPLING::InitLinkTable → RELATIVE
  5. COUPLING::TopologicalSort
  6. Bobject::MakeAbsolute → RELATIVE → ABSOLUTE

MARGBOX::getGenerationで停止した. (PHASE > GODOWNSTREAM)では「系列枠不在」という理由で止まった.つまり,この結婚枠は系列枠に所属していない.なぜこのようなものがあるのだろう?なんとこの結婚枠はCOUPLINGに直接接続している!(結婚枠は通常結婚リンクに接続している)隠蔽リストに乗っているようにも見えるが,hideflagはゼロ.YリストではTREEVIEWに直接接続している.Yが4という値を持っているので,隠蔽リスト上にあることは間違いない.ゴミと見て間違いないが,なぜこんなものがこんなところにあるのか?は追求されなくてはならない.

さて,ここまではできたが,その後が…

NODULE::operator []で例外が発生した 枝番はSEIZEGROUND

従来論理ではCOUPLING:EraseFamilyTreeでFAMILYTREEの下にあるすべてのスロットをパージしていた.この中にはTOPOLOGYやUNDOSYSTEMも含まれる.EraseFamilyTreeはデータファイルをクローズするたびに実行されているが,実際にはNOPで抜ける動作になっていて終了時に一度だけ実行されるようになっていた.これは関数の趣旨からしておかしいので,EraseFamilyTreeではシステムに1個しか存在しないようなオブジェクトはパージしないように仕様変更し,同時に冒頭で(PHASE <= INITIALSTATE)によって抜ける論理を廃止した.

TITLEBOXはシステムに1個しかないオブジェクトだが,描画オブジェクトはEraseFamilyTreeの中ですべてパージされるようになっているので,その規定に従って削除されるように変更した.この修正は「タイトル枠無しで動作する@20201119」でオプトアウトできる.

TITLEBOXが空でも動作するようになったが,削除されたTITLEBOXをどこかで補充しなくてはならない.どこでそれをやればよいのだろう?ファイルを開いたときどこかでTITLELINKを初期化ないし設定している場所があるはずだから,そこで行えばよいのではないか?TITLELINKクラスのメンバー関数には大したものが入っていない.

いや,その前にFAMILYTREE::InitializeFamilyTreeで(topology->PDB->getmaxrecn() || topology->MDB->getmaxrecn())が起きているという件を先に片付けておこう.これがそもそもの発端だ.EraseFamilyTreeは毎回実行されるようになったが,PDB,MDBが始末されていない.これはTOPOLOGYが管理しているものだ.その直前でTOPOLOGY::initializeが実行されているが,この関数は働いていない.

nodule::operator newのcheck文で(*moto)[edan]をチェックしようとしたところ,NODULE::operator []で例外が発生した.枝番はSEIZEGROUND.⇒SEIZEGROUNDはサポート範囲外.(*goo)[SEIZEGROUND]ではnullを返すようにした.

新規ファイルで起動→サンプルを開いて,COUPLING:CloseFamilyBase→…TREEVIEW::EraseTreeView…→TRIBEBOXのデストラクタで非参照カウントの残留が発生 TRIBELISTのスロット2からの参照が解決していない.bottom2からの参照だ.TRIBEBOXのデストラクタで始末するか,ないしTRIBELISTを先にキャンセルするか,あるいはその両方をやる必要がある.TRIBEBOX::DisposeではTRIBELIST::CleanSansyoを実行している.⇒LIST::CleanSansyoでbottom2をクリーンアップしていない.⇒対処した.

PDB,MDBをクリアする件に関してはEraseFamilyTreeとTOPOLOGY::initializeの両方に責任がある.しかし,TOPOLOGYはPDBとMDBの管理者ではあっても,所有者ではない.所有者であるLINKTABLEには何かチャンスがあるのだろうか?どこかにPDB,MDBを初期化する処理があってしかるべきなのだが… LINKTABLEにはdeleteCardData,DeleteMarriage,DeleteCardなどの個別操作を行う関数はあるが,テーブル全体をクリアするという操作はない.BASETABLEにはcleartableという関数はあるが,これは単純にカウンタをリセットしているだけだ.

CARDTABLEとMARGTABLEはBASETABLEから単純に派生しているので,BASETABLEにClearTableを作るのが早いと思う.CleanSlotで掃除してから,cleartableを実行すればよい.その上で,LINKTABLEでPDBとMDBをClearTable.これをEraseFamilyTreeから呼び出せば片が付く.⇒単純化し過ぎている.ARRAYスロット配列はそれでもよいが,BASETABLEは追加スロットとしてlongtable *lookupというのを持っている.これはカウントをリセットすればそのまま再利用できるが,オブジェクトを削除してしまうとまた,トラブルの元になる.⇒lookupが空になっているときにBASETABLEがどう振る舞うかを見ておこう.

lookupはBASETABLEのコンストラクタで生成されているだけなので,これが空になることは想定されていないと思う.LINKTABLEはMakingLookUpで初めてloopupテーブルを操作する.ここで停止するか,ないし新たに生成するべきだろう.⇒MakingLookUpではlookupテーブルをゼロクリアしてから使っているので,ClearTableでは放置でもよい.この暫定修正でPDBとMDBのクリアはできるようにあったが,(PDB->getmaxrecn() || MDB->getmaxrecn())で停止するという状況は変わらない.これはgetmaxrecnの仕様に問題がある.現行では

long getmaxrecn(void) { return min(maxrefnum, tablesize); }

という仕様になっているが,かなり疑問だ.BASETABLEはmaxrecnというメンバ変数を持っているのだから,素直にこれを返せばよいのではないかと思うのだが… ⇒同じ轍を踏まないように注意した方がよい.「その場の思いつき命取り」が怖い.明らかにこのロジックではレコード番号と参照番号という2つの番号を扱っている.どちらもテーブルのインデックスだが,おそらくレコード番号は連番で参照番号というのは飛び番になっているのではないかと思う.通常,getmaxrecnとmaxrecnの値は一致していると考えているが,何かの理由でそうなっていないのではないか?そもそも,lookupテーブルというのはどんな必要があって使われることになったのか?どういう使われ方をしているのか?⇒これは一覧表出力に関係している.

いずれにしても,上記getmaxrecnの定義は正しいと考えるしかない.従って,必要なことはcleartableでmaxrefnumをリセットすることだ.⇒これで一応動作するようになったが,lookupを残すように修正しておこう.BASETABLE::ClearTableで一律CleanSlotではなく,

ARRAY<bnum>::CleanSlot

のようにスロット配列だけのクリアに変更する.これで大体収まった.BASETABLE::lookupをMakingLookUpで復活させたように,TITLEBOXにも再生処理を作ってみよう.⇒TILELINK::gettitleboxは描画フェーズでTREEVIEW::Refreshから呼び出される他は,COUPLING::SetTitleBoxからの呼び出ししかない.従って,ここで再生するというのが最適と思われる.TITLEBOXを必要としているのは,TREEVIEWなので,TREEVIEW::gettitleboxで生成というのでよいのではないだろうか?⇒いや,TITLEBOXはTITLELINKのコンストラクタで生成されているので,やはり,TITLELINKの位置で生成するのがよいと思う.事前にTITLELINKが呼び出されるようなタイミングがあればよいのだが,どうも見当たらない,ということになればTILELINK:gettitleboxで生成するしかないだろう.

TREEVIEW::InitTreeViewというのがある.これは毎回呼び出されているのではないか?OpenFamilyTreeではInitCouplingとInitializeFamilyTreeを実行している.InitTreeViewはInitCouplingの中で実行される.TREEVIEWはCOUPLINGの所有物だ.⇒いや,おかしい.いつ入れたのだろう.TREEVIEW::InitTreeViewの中にはすでにその処理が組み込まれている.だとしたら,「タイトル枠無しで動作する@20201119」というオプションは元々不要だったということになるのだが… しかし,何らかの必要があっての修正だったはずだ… 例外をスローして初めて発現する障害だった可能性もある.

FORMALVERSIONのテストなのでまず,その設定に戻してみよう.⇒何も障害が発生していないのに,新規ファイルを開いてTITLEBOXが空になっている.これはかなりおかしい.TREEVIEW::InitTreeViewで生成されたTITLEBOXはどこに消えてしまっているのだろう?⇒タイトル枠設定で「タイトル枠を表示」をONにできない.それ以外のパラメータもすべて変更できない状態になっている.

▲★ASSERT_NEVER★ PHASE=22 funcname=nodule::Connect scount=447534 line=591, filename=nodule.cpp MakePairListを実行中,RepairPairBoxでノード対のチャンネル移動を実施してtakeout→ nodl_float→ Connectで(moto == nodl)が発生している motoとnodlはともに空

「増設スロット(EXTRA_ANCHOR)の子ノード」を親ノードに接続し,ゼロスロットノードをそのノードのゼロスロット末尾に移動しようとしている.ノード対リストではノード対を管理するために増設スロットを使っている.(EXTRA_ANCHOR)は①端点共有ノード対接続チェーンと②SymmetricActionでブロック移動実行時に使われるリストを一時的に接続するために使われている.どちらも参照ではなく接続チェーンで,①の場合はPAIRBOX,②の場合はnlist<Bobject>が接続される.

nodule::nodl_floatの論理は明らかに誤っている.というか混乱している.混乱しているのは「増設スロットの仕様」だ.増設スロットは一時的にある特定クラスのオブジェクトがそのグループ内で使うものだから,「どう使われてもよい」というようなところがあったのではないだろうか?どちらも「接続」だが,接続のトポロジーは異なる.

ノード対の場合は増設スロットを使ったチェーンになっているが,SymmetricActionの場合は増設スロットに接続されるのはnlistでその下にゼロスロットを使ったリストがぶら下がるという構成になっている.従って,nodl_floatではこれらを一律に扱うことができないばかりではなく,ノード対の場合には接続に使うスロットをゼロスロットと誤認している.自ノードのXスロットに接続するノードは前方ノードのXスロットに接続し,自ノードのZスロットに接続するノードは前方ノードのZリスト末尾に接続するというのが原則だ.

nodl_floatは全面的に書き改めるしかない.⇒修正完了したが,スタックオーバフローが起きている.完全参照リスト管理の操作を誤解していたかもしれない.いや,いまの修正は参照リスト管理とは関わりがないはずなのだが… どうもどこか完全に壊してしまったようだ…

例外処理を再整備する

条件コンパイル定義式の中にはASSERTIONマクロが含まれる.ASSERTIONは通常(つねにとは限らない)関数の入口に置かれて,引数の整合性などをチェックする一種の関門だ.ゼルコバの木ではASSERT_NEVERとDEBUG_NEVERという2つのマクロを使っている.マイクロソフトのASSERTIONは肯定型の言明で「かならず~であること」を確認するようになっているが,我々の場合には「決して~ではない」ということを検証する※.この違いは欧米的な意識と日本的意識の相違なのではないかという気がする.欧米感覚では「かくあるべき」という規範意識が強いような気がするが,日本的感覚だと,「こういうことは許されない」がそれ以外に関しては言及しない(なんでもよい).

※ASSERTIONとASSERT_NEVERは相互に転換可能だ.ASSERT(X) という言明は ASSERT NEVER EVER(~X)と等価である.

ASSERT_NEVERもDEBUG_NEVERもどちらも決して起きてはならないが,論理的にはまず起こらないと推定されるものだ.しかし,プログラムでは何が起こるか分からないので一次防禦線として張られている.DEBUG_NEVERはデバッグ時にのみ実行される内部検査で,仮に起きたとしても軽微なエラーとしてそのまま続行可能,ASSERT_NEVERは続行不能な致命的障害を意味する.ASSERT_NEVERはリリース版でもアクティブになっているので,事後処理つまり,例外処理が不可欠だ.例外処理はどうしても手抜きになってしまうところだが,いずれ整備しなくてはならない.旧版では「バグレポートの提出要請」という形で完全な例外処理が実装されていた.現行でも一部まだその機構は残っているが,これを再整備する必要がある.

昨日の終業間近に出ていたメモリアクセス違反を見ておこう.MARGBOX::setGenerationで例外をスローした後,新規ファイル生成不可→アプリ終了しようとしてGP例外が場外で発生する.ASSERT_NEVERが2回スローされているが,これは後で見ることにして… いや,2回どころか数回発生している.最後はCARDLINK:CleanSansyoで発出されたもので,例外はここで起きているものと見られる.ビルドはReleaseで,FORMALVERSION.Debugモードで再現できるかどうかを見てみよう.⇒GP例外は発生しない.その代わり,

Debug Error!
Program: D:\ZELKOVA\debug\ZelkovaTree2021.exe
abort() has been called

のような表示が出ている.「Debug Error!」という語句はゼルコバの木のものではないので,VSのデバッガが出しているのだろう.これでは手がかりにならないので,cleansansyo.cppの238行目からトレースしてみる.⇒~COUPLING→ ~FAMILYTREE→ ~CARDTABLE→ ~ARRAY→ CleenSlot→ ~CARLINK→ CleanSlot→ clearslot→ CleanSansyo のように進んだところで,RETURNIFCLEAN というマクロによってアボートしている.

#define RETURNIFCLEAN \
     ASSERT_NEVER (object == this && !(checkcid(‘n’) && ((NAMEBOX*)object)->getghostbits(SYMMETRICMARG|REALGHOST) == (SYMMETRICMARG|REALGHOST))) \
     if (!object || !object->Refcount()) return 0;

このマクロは各クラスのCleanSansyoの冒頭で実行されている.このマクロの趣旨は「objectが空かobjectの被参照カウントがゼロならゼロ復帰する」という妥当なものだが,その前に実行しているマクロが曲者だ.これは実行順序が逆だと思う.⇒修正したが,解決しない.⇒ASSERT_NEVERが成立して例外をスローすることでアボートしている.おそらく,VBが「プログラムを終了します」を出した時点でアプリ終了になっているはずだ.しかし,DLLには特に通知を送っていないので,それを検知することはできない.

いや,認識しているのではないか?PHASEはENDOFAPPLICATIONという状態になっている.~COUPLINGが実行されているのがその証拠だ.⇒ENDOFAPPLICATIONフェーズでは例外をスローしないとしておこう.確かに,それしか方法がないが,ではENDOFAPPLICATIONのときにはどうしたらよいのか?そのまま続行できないとすれば,リターンする以外ないが戻り値は各関数によってまちまちだ.⇒return 0; としてみたところ,void型の関数から一斉にクレームがあがった.

_ASSERT_NEVERというのがあるが,これはどういう用途で作られたものだろう?⇒デバッグモードではSTOPするか,_STOPかの違いだ.つまり,停止するか?警告パネルを出すかの違いだ.非公式版ではどちらも警告パネルを出すようになっている._ASSERT_NEVERを使っている事例を見ると主要なコンポーネントが存在することを確認するような使い方になっているので,これらを区別する必要性はあまりないような気がする.⇒_ASSERT_NEVERをvoid型で使うマクロとして再定義するというのでよいのではないか?やってみよう.

_ASSERT_NEVERが141個あった.⇒「コンストラクタは値を返せません」というのまで出てきた.確かにコンストラクタの中でASSERTIONするというのは自己矛盾であるような気もする.「オレは誰だ~」と言っているようなものだ.コンストラクタはそれ自身で完結しなくてはならない.⇒DEBUG_NEVERなら許されるだろう.⇒整数値以外を返す関数もある→3個.⇒付け替え完了し,ノーマルに終了できる._ASSERT_NEVERは定義を含めて367箇所,ASSERT_NEVERは2114箇所ある.Releaseビルドしたら漏れが見つかって,_ASSERT_NEVERが406個,ASSERT_NEVERが2075個に変わった.合計2481.

ReleaseビルドのFORMALVERSIONを起動して冒頭で例外が発生した TRASHCAN:MakeTrashCanの中で起きているように思われる

Debugビルドでは問題なく立ち上がってくるので,ASSERTIONの修正が関係している可能性が高い.「_DEBUGマクロを未定義にする」でどうなるか見てみよう.原因は分かった.コンストラクタの中で実行しているASSERT_NEVERをすべてDEBUG_NEVERに変えてしまったからだ.これらの中には単なる検査ではなく,

new (this, COUPLsKAKEIZU) KAKEIZU

のように実動作を伴うものがある.このような場合にはDEBUG_NEVERではなく,CONFIRM_NEVERを使わなくてはならない.TITLELINKでも同様のことが起きている.しまった.どのクラスのコンストラクタを修正したかメモっていない.以下のようなエラーが出ているので,これを片付けないと前に進めない.

image

ゼルコバの木で使っているクラスは49種ある.それを一つづつチェックするよりは,むしろ,DEBUG_NEVERを検索してしまった方が早い.DEBUG_NEVERは372個あるが,マクロの引数をチェックすれば修正が必要か否かはすぐに分かる.というか,このような誤りは別の場所にも存在する可能性があるので,全点検査はいずれにしても必要だ.DEBUG_NEVER (CheckFreeDirectSubTribe(funcname))のように検査関数を(検査モードで)呼び出すだけのものは見逃してもよい.しかし,これはかなりまずいのではないか?

DEBUG_NEVER (getkukan(primenod, realnode, true).Width())

getkukanの戻り値はBRectだ.いや,違う.この条件式はその矩形の幅がゼロか否かを見ている.⇒全点検査した.問題ないように思われる.⇒動作するようになった.上の不良はランニング中にソースコードを編集して続行したためではないかと思われる.⇒アプリを終了しようとしているのに,FAMILYTREEでInitializeFamilyTreeを実行しようとしているのはなぜか?このとき,

(topology->PDB->getmaxrecn() || topology->MDB->getmaxrecn())

が起きてASSERT_NEVERが再発動する.InitializeFamilyTreeの実行はアプリ終了後ではない.起動→新規ファイル→サンプルオープン→例外発生→検定失敗→新規ファイルオープンで実行される.しかし,起動時に新規ファイルが開けているのに,障害発生後にそれができなくなるというのもおかしい.ファイルのクローズを実行していないのではないか?⇒いや,実行されている.しかし,テーブルは空になっていない.

COUPLING::EraseFamilyTreeではフェーズがINITIALSTATE以下では入口で復帰している.これを廃止すればテーブルは空になるが,別のところでエラーが発生するようになる.Coupling::OpenFamilyBaseでpartialmap不在が発生する.COUPLING::OpenFamilyBaseではpartialmapが存在するものと仮定している.PARTIALNAMEはどこで誰が生成しているのか?FAMITREEの所有で,FAMILYTREE:InitializeFamilyTreeで生成され,FAMILYTREE:Disposeで削除される.TOPOLOGYも同様だ.CARDTABLEとMARGTABLEはLINKTABLEのコンストラクタで生成されている.

それではどこでPARTIALNAMEは消されているのだろう?⇒COUPLING:EraseFamilyTreeでFAMILYTREEのすべてのスロットをCleanSlotしている.COUPLINGがPARTIALNAMEを必要としているのは,ファイルの読み込み時に使っているからだ.部分図データはファイルのヘッダ部に保存され,PARTIALNAMEに直接ロードされる.それだけではない.TOPOLOGYもUNDOSYSTEMも,EraseFamilyTreeで消去しているほとんどすべてのオブジェクトが必要だ.ということは実質,EraseFamilyTreeは終了時以外は実行されていないということになる.呼び出しているフリはしているが,

if (PHASE <= INITIALSTATE) return;

でほぼ常時無動作でリターンしている.不要なときに空動作で戻るのはよいとしても,必要なときに動作しないのではまずい.見た限りではPARTIALMAPやTOPOLOGYは初回生成された場所以外では再生成されないので,EraseFamilyTreeで消去するというのは不当であるように思われる.これらのオブジェクトを消去しないという仮修正を入れて動作を見てみることにする.⇒かなりの修正が必要になったが,最後にTITLEBOXが存在しないという問題が残った.TITLEBOXはNAMEBOXやMARGBOXと同格の描画オブジェクトだが,かなり特殊な扱いになっていて,TREEVIEW直下のTITLELINKというのが管理している.

EraseFamilyTreeでは一旦すべての描画オブジェクトをパージしてしまうので,その中にTITLEBOXが含まれているため,TITLEBOX不在が至るところで発生することになる.⇒この件に関してはむしろ,まずタイトル枠が存在しない状態でも動作するように仕様変更した方がよいと思う.タイトル枠の仕様はまだ,今後も変化する可能性があるのでそのオブジェクトが一つ欠けているだけでシステムが停止してしまうというのはあまりよい仕様ではない.⇒一応エラーなしに動作するようになったが,画面が真っ白のままだ.メニューなどは操作できるので,システム的には動作しているようだが,画面上に何も描画されていないことは拡張選択しても何も選択できないことからも間違いない.

原因は分かった.COUPLING:SetTitleBox→ TITLEBOX:SetDispParmを呼び出していない.この中にはTREEVIEWの計算が入っている.TREEVIEWに関わる部分を切り出して別途実行する必要がある.系図外枠の計算を行っているTREEVIEW::GetTitlePositionの修正も必要だ.⇒一通り修正を入れたつもりだが,画面が出ない…タイトル枠を表示しないというモードがあるくらいなのだから,タイトル枠がなければ描画できないということはないはずだ.

!描画できた!

また条件コンパイルマクロの集計が合わなくなった

comdebug.hでCOMDEBUG: タグ付けの誤りが見つかったので修正した.これでまた条件コンパイル定義文の集計が合わなくなった.原因を調べる必要がある.正規表現で値なし定義文を検索した結果は131件で変わらない.COMDEBUG: は1個減って27個になっている.どこかで間違えているはずだ.⇒昨日はフィルタリングした状態では連続データの入力(ナンバリング)ができなかったが,メニューから実行する方法がある.また英語版に戻しているが,Edit→Fill→Seriesが使える.ただし,実行する前に複数セルを選択しておかないとコマンドが出てこない.⇒以下の手順で簡単にチェックできる.

  1. VSで値なし定義文を正規表現を使って検索し,テーブルに出力
  2. (1)をOpenOfficeに読み込み,COMDEBUG: でフィルタリング
  3. VSでCOMDEBUG:で全文検索し,テーブルに出力
  4. (3)を(2)の後ろに貼り込み
  5. (4)をソートして,ダブっていない行を探す

いや,おかしい.COMDEBUG:で検索した結果も27で合っている.上の手順で比較しても完全に一致している.ということは別のタグで不一致ないし増減が起きていると考えるしかない.調べてみよう.SPECIFICATION:は16で昨日と同じだ.OPTIONS:(11),DEBUG:(23),VERIFY:(5),TEST:(11),LOCAL:(10),_H_ (27)ですべて同じだ.トータル130で1個過不足がある.つまり,#define文の方が多いということになる.これを見つけるのは容易だ.タグがついてないものを探せばよい.あった.HOSTCMD.H冒頭のマクロだ.

#if !defined(__HOSTCMD_)
#define    __HOSTCMD_

これは,

#if !defined(__HOSTCMD_H_)
#define    __HOSTCMD_H_

のように書かれなくてはならない.ヘッダファイル冒頭のマクロは以下の式で正規表現のオプションで検索している.

検索:#define.*_H_         (H)ヘッダファイル冒頭の定義文を検索

これで_H_の個数は28となり,トータル131で完全に一致した.昨日の数字が合っていたのも,単なる偶然ということになる.やれやれ…

ダメだ.まだ合っていない.上記の検索:#define.*_H_の行が検索結果に含まれてしまっている.これを除くと27個,まだ総数ではあっていない.この行がヒットしないように検索キーを次のように変えてみた.

検索:#define\s+.*_H_   (H)ヘッダファイル冒頭の定義文を検索

これで件数は27という正しい数字になったが,タグ付きの総数は130で定義文の個数131と合わない.ヘッダファイル定義のミスがもう1件あった.Lineage.hで_LINEAGE_Hになっている.⇒これで完全に一致した._H_の個数は28が正しい.

さて,「制御が渡らないコードです」の問題を片付けなくてはならない.昨日のログでは,後付けで「※理由は簡単,STOPで例外をスローしているのは「公式版」だけだ」としているが,リリースモードでFORMALVERSIONとしてもこの警告は表示されない.リリースモードとデバッグモードでは警告の出方が多少異なるので「そんなものか」とも思うが,普通はリリースモードの方が厳格になっているのに,その逆というのが納得いかないところだ.

警告レベルを下げるなどの対処法は芳しくないので,昨日述べたように,例外をスローするのはASSERTIONに限定することにする.⇒実装してみたが,悪くないように思われる.Stopはどうだろう?デバッグモードではSTOPと同じ効果だ.内部でSTOPを呼び出している.Stopは引数で文字列を渡せるので,STOPより詳細な情報を得ることができる.公式版の場合はデバッグ時と同様の動作でよいのではないか?Stopは関数でマクロではない.どこかでデータ退避.ZELへの書き込みが発生している.⇒呼び出されてはいるがパスしている.SAVERESCUEFILEがデバッグ時しか有効でないためだ.なぜだろう?理解できない.

公式版ではalartprintでパネルを表示しないようにしたので,STOPと_STOPを公式版だけ別扱いする必要はなくなった.この2つは共通定義でよいのではないか?⇒いや,そういう訳にはゆかない.デバッグモードでは停止しなくてはならない.⇒STOP,_STOP,Stop の動作はまずまずなのではないかと思う.デバッグモードではある程度情報を取得できるようにした.ただし,「_DEBUGマクロを未定義にする@20170811」ではほとんど何の情報も得られない.アプリは停止しないのでまるで問題なく走っているかのようだ.

ASSERT_NEVERを試してみた.なんとあの懐かしいパネルが飛び出してきた.標的型攻撃を受けてお払い箱になっていたあのカマキリだ.

image

しかし,このあとが相当悪い.パネルを開くと同時にユーザ会サイトのバグレポートにアクセスして「インターネットに接続されていません」になるのはよいとしても,パネルを閉じた後にSTOPパネルを2回開いたあと,以下のパネルが順番に出る.

image

image

image

image

image

新規ファイルのオープンに失敗した場合には打つ手がないのでアボートするというのでよいと思うので,新規ファイルだけは作れるような場所で例外が発生するようにしてみよう.PARTIALNAME:PartialMapCommandに例外を仕掛けてみたが,パスが通っていないようだ.どこかで握りつぶされている.mmm…今度はパネル1枚出さないでアプリ終了してしまった.この例外は全くトラップされずにストレートにGCまで戻ってしまっているようだ.

上の現象は再現できない.確かにmZelkova::mPartialMapCommandにはエラートラップが仕掛けられていない.GCでは61個の関数にエラートラップが設置してあったのだが,肝心なところが抜けていた.エラーを返してやっても以下のパネルが出るだけだ.

image

これはVB側の判断でエラーで帰るとこのパネルを出して抜けるようになっている.ここでは,⇒「検定に失敗しました」を出してもらいたいところなのだが…検定に失敗しましたは,InitializeDisplayで出しているものだ.系統並び替え実行後はかならずここを通るので,ここで「検定に失敗しました」を出すのは妥当だろう.ユーザは何度も「部分図タイトルを選択してください」が出れば,おかしいと思って問い合わせしてくるかもしれない.しかし,その前にユーザにASSERTIONで失敗したということが伝えられていなければならない.

GCではエラーをキャッチしてはいるが,エラーの種別には無頓着なのでエラーコードを取り出すこともできない.これはやはりDLL側で受けるしかないのではないだろうか?GCとDLLを接続する関数は1対1になっているので,対応するにはすべての場所で個別対応するしかないが,幸い部分図はコマンド処理になっていてすべての操作が一つの関数を通るようになっているので,対応は容易だ.

PARTIALNAME::PartialMapCommandの後半部,実際にコマンドを実行している部分にはすでにトラップは仕掛けられているので,前半部にだけ掛けてみよう.おかしい.なぜだろう?キャッチできない.どこかでSTOPしていることは確かだが,FORMALVERSIONなので停止しない.⇒エラートラップにSTOPが置いてあった.

ここでもう一度例外をスローしてそれがGCに入ってくる.例外をそのまま転送するというのはそれでよいとしても,ここで何らかの処理をしないと何が起きているのか分からない.一番早いのはASSERT_NEVERで直接処理することだろう.ASSERT_NEVERの内部で処理するとしたら,マクロではなく関数化した方がよいのではないだろうか?しかし,関数化してしまうと,障害が発生した場所が分からなくなる.関数名くらいなら渡してももよいが…⇒マクロはマクロとして残して内部処理の部分を関数化しておくというのが扱い易いのではないか?情報はマクロから関数の引数で渡してやればよい.⇒それがよいもしれない.

ERR_ABORTEXPERIMENTというのがすでにある.何をやっているのか見てみよう.⇒こんなことがあるのではないかと思っていたが,やっぱりエラーコードのダブリがあった.ERR_CARDTABLEOVERとERR_SHOWUNDERWEARがかち合っていた.エラーコードは一応カテゴライズされているのだが,なぜか大きい番号があちこちに散らばっている.⇒ERR_SHOWUNDERWEAR,ERR_ABNORMALGENEGAP,ERR_ABORTEXPERIMENTなどをキャッチしたときには,例外を先送りしている.ERR_ABORTEXPERIMENTをキャッチしている2箇所で,ERR_ABORTPROCも同じ扱いになるようにした.

PartialMapCommandの前半と後半の2つのトラップは一つにまとめてよいと思う.一応の段取りはできたが,最初のエラーパネルが多重に出てくる状態を再現できるだろうか?例外を部分図のロードに仕掛ける前だから,MakeUpTreeかないしその前と思われる.⇒場所が見つからない.上記でエラートラップ2箇所では例外を先送りするように修正しているので,処理されているのかもしれない.⇒少なくとも処理されずにゼロ復帰している箇所はある.すべてのエラートラップで例外を先送りするようにしておく必要がある.catchで検索すると207箇所もある.整数型以外のエラーをキャッチした場合には警告パネルを出すようになっているので,整数型だけ見ればよいと思う.

THROWFATALERRORというマクロを作って,ERR_SHOWUNDERWEAR,ERR_ABNORMALGENEGAP,ERR_NOMEMORY,ERR_ABORTEXPERIMENT,ERR_ABORTPROCでは例外を先送りするようにしてみよう.整数型エラーをキャッチしているところは74箇所ある.⇒TRASHCAN::throwCanのようにすべての例外を投げ出しているところはそのままとする.⇒対応修正完了した.

▲potential.cppの707行目で例外を発生させるコードをリリース版で走らせると,COUPLING::TopologicalSortのエラー→検定に失敗しました→新規ファイルのオープンに失敗しました→プログラムを終了しますの後にどこか不明の地点で例外が発生する.

サーカスで命綱を使ったら笑いものになる

条件コンパイル文はプログラム開発の分岐点に相当する.この分岐点がゼロ個になった状態が「正式版」であるとして,その個数を削減する作業を進めてきた.「再開発スタート版」の時点で206個あった値を持たない#define定義文は現在131個まで削減された.さらにこれらを整理して以下の4つのヘッダファイルで区分管理することにした.

  1. nodule.h SPECIFICATION:(16), OPTIONS:(11), PENDING:(0), INCOMPLETE:(0)
  2. comdebug.h COMDEBUG:(36)
  3. Bobjecth DEBUG:(23), VERIFY:(5)
  4. coupling.h TEST:(11)
  5. *.cpp LOCAL:(10)
  6. *.h _H_ (26)
  7. total (138)

mmm… 合計が一致しない!定義文の個数は正規表現を使って検索している.なにか漏れがあるのだろうか?漏れがあるとしても,漏れているものを特定できないことには対策の打ちようもない.VSの検索では検索結果をテーブル形式で出力できるので,それを表計算に持ち込んで並び替えなどしてみれば何か分かるかもしれない.開発機をネットに出して,Open Officeをダウンロードしてこよう.⇒横文字に慣れるために今回は英語版を使ってみることにした,ところまではよかったのだが,連続データの入力ができない.普通表計算ではドラッグして連続した数値を自動入力することができるのだが,なぜか同じ数字の並びになってしまう.Open Office のバージョンは4.1.8だ.ネットを見ても「できる」としか書いてない.仕方ないので,日本語版に差し替えてみた.

インストールしてまだ空のファイルを開いた状態で一番上のセルに1と入力し,右下の+をつかんで下にドラッグするときれいに1, 2, 3… の数字が入る.これは締めたと思い,COMDEBUG: という文字列を含む行だけフィルタリングしてもう一度ナンバリングしようとすると,元の状態に戻ってしまっている.つまり,同じ数字が入ってしまう.しかし,最初はできていたのだからできない訳はないだろうと考えて,フィルタリングの結果を別のシートにコピーしてもう一度試すと今度はうまくいった.確かにフィルタリングした状態というのは一部データだけを表示した状態なので,その一部データに適切な番号を割り当てるというのは多少問題のある操作かもしれない…

これで検索テーブルのデータとタグで直接検索した結果を突き合わせた結果,どこで問題が発生しているかを大体突き止めることができた.まず.値を持っているマクロ定義文にタグを付けていた.たとえば

#define probe {}   // COMDEBUG: ← タグ 

のような感じだ.探しているといろいろなアラも見えてきた.ヘッダファイルの冒頭には必ず,以下のような行を入れて,ヘッダファイル全体を囲むようにしているが,_H_の後ろの「_」が欠けているのが複数箇所で見つかった.

#if !defined(_◯◯◯_H_) ◯◯◯はヘッダファイル◯◯◯.hの語幹部
#define _◯◯◯_H ← 「_」が落ちている
 ︙ヘッダ本体
#endif

そのほかにも

#define { \ ← 「{」というマクロを定義したことになる?

のようにキーワード自体が消失しているマクロすらあった.それらを修正したり,などいろいろやっているうちにいつの間にか comdebug.h の大掛かりな書き直しに発展してしまった.実際,今回のセッションではnodule.hからcomdebug.hにはかなりの定義文を移動しているが,comdebug.hにはまったく手を付けていなかったのでいずれやらざるを得ない仕事ではあったのだが… そんな訳で検索で抽出した定義文の個数と個別にタグを検索した結果が完全に一致するまでに丸一日掛かってしまった.最終的な結果は定義文131個で変わらず,内訳は下記の通り.★はデフォルトON,☆はOFF.■はリリース版でON,□は混合,・はデバッグ用でリリース版ではOFF.

  1. ■ nodule.h ★SPECIFICATION:(16), ☆OPTIONS:(11), PENDING:(0), INCOMPLETE:(0)
  2. □ comdebug.h COMDEBUG:(28)
  3. ・Bobjecth ☆DEBUG:(23), ★VERIFY:(5)
  4. ・coupling.h ☆TEST:(11)
  5. ・*.c LOCAL:(10)
  6. *.h _H_ (27)
  7. total (131)

SPECIFICATIONはデフォルトONで確定している(つまり分岐なし),OPTIONSはデフォルトOFFで未確定だが,安定版ではOFF,COMDEBUGはモードにより変わる,DEBUG,VERIFY,TESTはデバッグ時のみでリリース版ではすべてOFF.この整理によって条件コンパイル文は一意に確定し,リリース版の分岐はゼロになったと認定できる.この過程でこのシステムには4つのモードがあることが分かった.

  1. デバッグ版モード(Debug) _DEBUG
  2. 疑似リリース版モード(Debug) _RELEASE
  3. 非公式版モード(Release) 定義なし
  4. 公式版モード(Release) FORMALVERSION

Visual Studioでは開発時には(Debug)モードでビルドし,配布版をビルドするときには(Release)モードでビルドする.(Debug)モードに切り替えると,システムが自動的に_DEBUGを定義してくるので(定義済マクロ),それをソースコードの中で使って

#ifdef _DEBUG
︙データをダンプしたりなど,デバッグ時に行うこと…
#endif

のようなことができる.これでほとんどのバグはシューティングできるのだが,まれにリリース時にのみ起きるバグというのが発生することがある.(2)疑似リリース版モードというのはこのような事態に対処するためのモードで,_DEBUGを#undefすることによって,デバッグ時にのみ走るコードを抑制してリリース版と同じ条件で走らせるというモードだ.このようなことが起きるのはデバッグ用コードに動作に影響するような部分が混入しているということを意味するが,それを目視で切り分けるのはかなり難しい.

非公式版モードと公式版モードの違いは所内版と一般公開版の違いと言える.非公式版はデバッグ版から_DEBUGのブロックを除いただけなので,ほぼ同じものと考えて間違いない.デバッグ版ではエラーが発生するとSTOP文で停止するようになっているが,リリース版では割り込みは使えないのでパネルを出して停止するというくらいの違いだ.

ReleaseモードでDLLをビルド中BugReportDialog.cppのコンパイルでエラーが発生した

1>d:\zelkova\zelkovadll\src\bugreportdialog.cpp : fatal error C1001: コンパイラで内部エラーが発生しました。
1>(コンパイラ ファイル ‘d:\agent\_work\18\s\src\vctools\compiler\utc\src\p2\main.c’、行 187)
1> この問題を回避するには、上記の場所付近のプログラムを単純化するか変更してください。詳細については、Visual C++ ヘルプ メニューのサポート情報コマンドを選択してください。またはサポート情報 ヘルプ ファイルを参照してください。
1>  link!InvokeCompilerPass()+0x2f79a
1>  link!InvokeCompilerPass()+0x2e9f8
1>  link!CloseTypeServerPDB()+0xd5266

このファイルは開いてもいないので,もちろんどこもいじったりなどしていない…もう一度単独でコンパイルしたら,何のエラーもなく終わった.いや,ビルドすると再発する.⇒クリーンビルドしたら解消した.どうもオプティマイザで出しているエラーのようだ.

FORMALVERSIONではSTOPで停止する代わりにエラーパネルを出した後,例外をスローするようにしたが,SUWのパネルが出てしまう.⇒ERR_SHOWUNDERWEARとERR_ABORTPROCでエラーコードがかぶっていた.別のコードを割り当てることでSUWは出ないようになったが,最後のパネルが出るまでにエラーパネルが数段に渡って出る.「警告パネルを重複表示しない」というオプションはSPECIFICATIONに入っているのだが,さっぱり効いていないようだ.⇒このオプションではbugflagを操作しているが,置いてある箇所がSaveRescueFileの中で,リリースモードでは無効になっているブロックだ.

AlartPrintはこのフラグを見てリターンするようになっている.alartprintも同様の動作になっている.このフラグは「障害発生時の緊急退避ファイル保存の場合は例外をスローしない」という趣旨のもので一般的にエラーパネルが多段に出ることを抑制するものではない.⇒STOPでパネルを出さずに例外をスローするだけにして,パネルはcouplingが出しているものだけになった.

image

この後,VBでもエラパネルを出してくるが,まぁそれくらいはよいのではないかと思う.

image

アプリはアボートしないでその後も動作可能だが,DLL側で終了時に参照カウントの残留エラーが発生して,複数回エラーパネルが表示される.どうしたらよいか?このパネルはalartprintで出しているものだ.bugflagが立っている場合には表示されないのだが…FORMALVERSIONのときはalartprintを出さないようにすると,上のVBのパネルしか出ないようになる.これはこれでもよいような気はする…

参照カウントの残留エラーはDECOMPOSITIONフェーズで出ているので,これ以下のフェーズではパネルを出さないというようにしてみよう.⇒これでエラーパネルはcouplingとVBで出すものだけになった.最終的な形は分からないが,一応エラーが発生したときの道筋は通ったのではないかと思う.FORMALVERSIONのときのエラー処理は一応目処が立ったが,一つだけ問題がある.

FORMALVERSIONと_DEBUGが共存できない.あるいは,FORMALVERSIONと_RELEASEでもよいのだが,リリースモードではブレークで止めたりなどのことができないので,デバッグに相当な制約がある.問題はこのような設定だと,「warning C4702: 制御が渡らないコードです。」のような警告が出て(警告をエラーとして扱っているため)ビルドが通らないというところにある.リリース版でも状況は同じだと思うのだが,リリース版ではこの警告は出ない.いや,もちろん下記のようなコードでこのエラーが出るというのはわかるけど,なぜそれがリリース版では出ないのか?というところがわからない…※

※理由は簡単,STOPで例外をスローしているのは「公式版」だけだ.

if (gene != gbox->boxgene) {
     STOP; ← ここで例外がスローされる
     gbox->setGene(gene); ← ここには制御が渡らない
}

FORMALVERSIONの場合は,ここでSTOPすると例外をスローするので,次の行に到達できない.FORMALVERSIONでなければ,一旦停止したあと,処理を再開することもできる.実際,いま設置しているSTOP文は軽い気持ちで入れているところは多いので,アプリ実行時に予定していないところでアボートしてしまうということも大いにありそうだ.この意味ではFORMALVERSIONで例外をスローする場所をもっと狭める必要があるのではないか?基本的にASSERTIONで監視している条件は100%致命的なので,ここで例外をスローするというのは当然であり,必要と思われる.それ以外は無視でもよいのではないだろうか?現行ではASSERTIONはどのモードも共通に

#define ASSERT_NEVER(assertion) {if (assertion) STOP }

となっているが,これを

#define ASSERT_NEVER(assertion) {
 if (assertion) {
#ifdef FORMALVERSION
  throw ERR_ABORTPROC;
#else
  STOP
#endif
 }
}

のような感じで書き換えればよいのではないだろうか?それではSTOPでは何をすればよいのか?STOPは元々デバッグ時に止めるという趣旨で入れているのだから無動作でよいはずだ.その代わり,SUWなどの動作はデバッグ時と完全に同じになるから,ShowUnderWearで下着を見られてしまう可能性もある.それはそれでよいのではないだろうか?一般公開とは言えまだしばらくは一部ユーザに使ってもらう段階だし…

逆に言うと,これまでASSERTIONで停止したことはただの一度もないと思われるので,逆にFORMALVERSIONではASSERTIONを完全に無動作にしてしまってもよいのではないかとさえ思えるのだが…高いところで仕事するのに命綱を使うのは悪いアイディアではないが,サーカスで命綱を使ったら笑いものになる…とは言え,サーカスでもブランコの下にはセーフティネットが張られていたかもしれない… マイクロソフトのASSERTIONはリリース版でも作動していたような気もする…