[今日のPiet]スタック内のランダムアクセス(4)

更新放置し早2週間、はてなさんにまた声かけられる前に、

前回の続きを書くことにします。

概要

Pietのスタックを、あたかも通常のメモリのように使うべく、

メモリ操作の基本となる「メモリアドレス変換関数」を作っていました。

解説が長くなったため、途中で切り上げてました。

今回は解説の後半です。

解説

例によって解説が長いので、折りたたみました。

興味のあるところをどうぞ。

スタックの変更準備(35-45行目、:ptr_calcから:ptr_emb_num_memsの前まで)

アクセスするメモリの番号mとアドレスnが正しいということが前回までで確認できましたので、

いよいよスタックを本格的に操作し、メモリ・アドレス変換関数の結果Sの計算に入っていきます。

ここでは、スタックをrollで操作するための準備をするのですが、

ここでもサブルーチン:ptr_sub_Rに飛びます。

最初のスタックの状態は次の通りです。

f:id:y-mos:20210731130719p:plain

サブルーチン:ptr_sub_Rに飛んだ後のスタックの状態は次の通りです。

f:id:y-mos:20210731143558p:plain

この状態から、今後の処理で必要になるRMの値を、必要な数だけ複製していきます。

f:id:y-mos:20210814191029p:plain

複製したRMのうち1組を、メモリ要素数の配列の先頭N_1の下に移動させます。

N_1の下に移動させたRMは、スタックの状態を元に戻すときに使います。

移動の操作をするため、roll命令とdup命令などを繰り返して、スタックを次の状態にします。

f:id:y-mos:20210814191159p:plain

この状態からroll命令を実行して、ピンク色のRMを、N_1の下に移動させます。

f:id:y-mos:20210814191325p:plain

sum関数の引数準備(46-50行目、:ptr_emb_num_memsから:ptr_move2topの前まで)

メモリアドレス変換では、メモリの位置Sを計算する必要があります。

S=N_1+\cdots+N_{m-1}+n

これを計算するにはまず、メモリ1からメモリm-1までのメモリ要素数の総和、

すなわち、N_1+\cdots+N_{m-1}を求める必要があります。

これには以前作ったsum関数を利用します。

ymos-hobby-programing.hatenablog.com

sum関数を使うときは、スタックトップを次の状態にする必要があります。

f:id:y-mos:20210814195204p:plain

a_1、・・・、a_nは、総和を求める配列ですので、

今の場合は、N_1、・・・、N_{m-1}に対応します。

それとは別に、配列{{a_n}}の要素数nも引数として与える必要があります。

今回の場合、このnに該当する値は、m-1です。

したがって、この引数m-1を準備する必要があります。

まずは、スタック内のmをスタックトップに移動した上で複製し、一方を元の位置に戻します。

そして、スタックトップに残ったmから、m-1を計算します。

f:id:y-mos:20210814192230p:plain

同様にして、スタックトップにR+71を用意します。

f:id:y-mos:20210814192706p:plain

この状態でroll命令を実行すると、先ほど準備したm-1が、

メモリm-1の要素数N_{m-1}の直上、メモリmの要素数N_mの直下に移動します。

f:id:y-mos:20210814193023p:plain

sum関数計算対象をスタックトップへ(52-54行目、:ptr_move2topからgoto:ptr_sumの前まで)

いよいよsum関数を適用する部分をスタックトップに移動します。

まずはスタックトップ付近のRMの値から、roll命令の引数を計算します。

f:id:y-mos:20210814193652p:plain

この状態でrollを実行すると、メモリの要素数の配列のうち、

N_1、・・・、N_{m-1}と、それらの個数m-1がスタックトップに移動します。

f:id:y-mos:20210814193954p:plain

なお、先ほどN_1の直下に移動したRMも一緒に移動させています。

メモリ要素数の配列のうち、N_m、・・・、N_Mの部分は、今は使いません。

これらは図のように、スタックの深いところに隠れた状態になります。

ここまででsum関数を適用する準備ができました。

sum関数の適用(56-57行目、goto:ptr_sumから:ptr_sum_retの前まで)

いよいよsum関数を適用します。

goto:ptr_sumの直前の状態は以下のようになっています。

f:id:y-mos:20210814193954p:plain

ここでスタックトップの部分のみに着目します。

f:id:y-mos:20210814194944p:plain

さて、sum関数を開始する時のスタックの状態は、次の状態になっている必要があるのでした。

f:id:y-mos:20210814195204p:plain

これらの画像を比較すると、

  • sum関数の入力a_1、・・・、a_nが、それぞれ、N_1、・・・、N_{m-1}に対応
  • sum関数の入力n(=配列の長さ)が、m-1に対応

と対応することがわかります。

したがって、現在のスタックの状態でsum関数を実行すると、

スタックトップからm-1個の要素、すなわち、N_1、・・・、N_{m-1}の合計Sが計算され、

その結果Sがスタックトップにプッシュされます。

(正確には、「プッシュされた状態になります。」)

f:id:y-mos:20210814195803p:plain

したがって、goto:ptr_sumが実行され、処理が終わると、

スタックが次の状態になって:ptr_sum_retに戻ってきます。

f:id:y-mos:20210814194844p:plain

この時のSは、メモリ1からメモリm-1までのメモリ要素数の総和です。

つまり、

S=N_1+\cdots+N_{m-1}

です。

スタックの原状復帰(58-67行目、:ptr_sum_retのあとから:ptr_add_ofsの前まで)

メモリ要素数の総和Sが、「ほぼ」計算できたので、スタックの状態を元に戻します。

まずは計算結果Sを退避します。

計算結果Sは最終的にはスタックトップの付近に来るようにしたいので、

黄色のnの直上にSを移動させます。

幸運なことに、スタックトップから黄色のnまでの深さは、

スタックトップ付近にある値m-1から計算することができます。

そこで、m-1を複製しながら、スタックトップを次の状態にして、

f:id:y-mos:20210814200734p:plain

roll命令を実行します。

f:id:y-mos:20210814203157p:plain

これで、Snの直上に来ました。

次に、最初に埋め込んでおいたピンク色のRMを、

N_1の下からスタックトップに移動させます。

ピンク色のRMまでの位置も、先ほどと同様にm-1から計算できます。

f:id:y-mos:20210814203403p:plain

今回は2つ値を掘り起こすため、スタックトップの値は負数「-2」とします。

この状態でrollを実行すれば、ピンク色のRMがスタックトップにきます。

f:id:y-mos:20210814203532p:plain

そしていよいよ、スタックの原状復帰です。

RMから、rollの引数を計算し、

f:id:y-mos:20210814203637p:plain

rollを実行すると、

f:id:y-mos:20210814203704p:plain

分離していた、メモリの要素数の配列N_1、・・・、N_{m-1}と、

N_m、・・・、N_Mが再び連結されます。

同時に、sum関数の出力結果Sが、スタックトップに現れます。

これで、今回の関数の峠は越えました。

Sの計算仕上げ(68-70行目、:ptr_add_ofsから:ptr_dup_Nの前まで)

さて、今のSは、まだ本当に求めたいSではありません。

今のSの値は次の通りです。

S=N_1+\cdots+N_{m-1}

一方で本当に求めたいSの値は以下の通りです。

S=N_1+\cdots+N_{m-1}+n

つまり、まだnが足されていません。

nはスタックトップ付近にあるので、この補正はそんなに難しくありません。

スタックトップにnを複製して、

f:id:y-mos:20210814204319p:plain

add命令を実行します。

f:id:y-mos:20210814204407p:plain

これでSの計算は完了しました。

戻り値の生成(71-77行目、:ptr_dup_Nからgoto:ptr_endまで)

大詰めです。

今回の関数の戻り値はSだけではありませんでした。

f:id:y-mos:20210731114729p:plain

この図のように、NMFを追加する必要があります。

それぞれの意味は、過去記事をご参照ください。

ymos-hobby-programing.hatenablog.com

ymos-hobby-programing.hatenablog.com

これらの追加は難しくありません。

NMについては、スタックにある値をコピーすればOKです。

Fは、「処理がうまく行ったぜぇ!!」ということを表すために

定数1とするだけなので、素直に1をプッシュします。

そして、やっと、ようやく、:ptr_endに飛んで、

全ての操作が終了です。

お疲れ様です!!

補足:エラー処理(79-81行目、:ptr_errからgoto:ptr_endまで)

最後にエラー処理について補足します。

エラー処理は79-81行目で実施します。

21行目、または、35行目で、メモリ範囲外にアクセスしようとした時に、

ここに飛んできます。

飛んできた時のスタックの状態は次の通りです。

f:id:y-mos:20210731130719p:plain

出力時の状態は、次の通りですので、

f:id:y-mos:20210731114729p:plain

SNMFを足す必要があります。

仕様上、エラーが発生した場合はこれらの値は全て0となるので、

素直に0を4回プッシュします。

なお、0は直接プッシュできないので、適当な値(ここでは1)をプッシュして複製し、

それらの差をとって0として、3回複製していきます。

出力(153行目から最後まで、:ptr_end以降)

最後に、スタックから値を4つ取り出して表示しています。

半角スペース区切りで出力するため、値をひとつ出力するごとに、

スタックに半角スペースの文字コード=32=4\times{4}\times{2})を作り、出力しています。

スタックトップから出力されるため、出力順は、

F~M~N~S

となります。

最後に

書いたよ。書き上げたよ。めっちゃ疲れたわ。

いつも思うんですが、こんな説明で大丈夫だろうか?

まあなんとなく雰囲気を掴んでもらえればよしとするか。

今回の解説ですが、ちょっと難儀したので、

次回以降、何をどう説明するかを考えないといけないなぁと思います。

あと、定期的に更新しないとなあ、と反省仕切りの今日この頃でした。

もうちょっと負荷の少ない書き方しないと続かんね、やっぱり。

では。