[今日のPiet]スタック内のランダムアクセス(4)
更新放置し早2週間、はてなさんにまた声かけられる前に、
前回の続きを書くことにします。
概要
Pietのスタックを、あたかも通常のメモリのように使うべく、
メモリ操作の基本となる「メモリアドレス変換関数」を作っていました。
解説が長くなったため、途中で切り上げてました。
今回は解説の後半です。
解説
例によって解説が長いので、折りたたみました。
興味のあるところをどうぞ。
スタックの変更準備(35-45行目、
:ptr_calc
から:ptr_emb_num_mems
の前まで)
アクセスするメモリの番号mとアドレスnが正しいということが前回までで確認できましたので、
いよいよスタックを本格的に操作し、メモリ・アドレス変換関数の結果の計算に入っていきます。
ここでは、スタックをrollで操作するための準備をするのですが、
ここでもサブルーチン:ptr_sub_R
に飛びます。
最初のスタックの状態は次の通りです。
サブルーチン:ptr_sub_R
に飛んだ後のスタックの状態は次の通りです。
この状態から、今後の処理で必要になるとの値を、必要な数だけ複製していきます。
複製したとのうち1組を、メモリ要素数の配列の先頭の下に移動させます。
の下に移動させたとは、スタックの状態を元に戻すときに使います。
移動の操作をするため、roll命令とdup命令などを繰り返して、スタックを次の状態にします。
この状態からroll命令を実行して、ピンク色のとを、の下に移動させます。
sum関数の引数準備(46-50行目、
:ptr_emb_num_mems
から:ptr_move2top
の前まで)
メモリアドレス変換では、メモリの位置を計算する必要があります。
これを計算するにはまず、メモリ1からメモリm-1までのメモリ要素数の総和、
すなわち、を求める必要があります。
これには以前作ったsum関数を利用します。
ymos-hobby-programing.hatenablog.com
sum関数を使うときは、スタックトップを次の状態にする必要があります。
、・・・、は、総和を求める配列ですので、
今の場合は、、・・・、に対応します。
それとは別に、配列の要素数も引数として与える必要があります。
今回の場合、このに該当する値は、です。
したがって、この引数を準備する必要があります。
まずは、スタック内のをスタックトップに移動した上で複製し、一方を元の位置に戻します。
そして、スタックトップに残ったから、を計算します。
同様にして、スタックトップにとを用意します。
この状態でroll命令を実行すると、先ほど準備したが、
メモリm-1の要素数の直上、メモリmの要素数の直下に移動します。
sum関数計算対象をスタックトップへ(52-54行目、
:ptr_move2top
からgoto:ptr_sum
の前まで)
いよいよsum関数を適用する部分をスタックトップに移動します。
まずはスタックトップ付近のやの値から、roll命令の引数を計算します。
この状態でrollを実行すると、メモリの要素数の配列のうち、
、・・・、と、それらの個数がスタックトップに移動します。
なお、先ほどの直下に移動したとも一緒に移動させています。
メモリ要素数の配列のうち、、・・・、の部分は、今は使いません。
これらは図のように、スタックの深いところに隠れた状態になります。
ここまででsum関数を適用する準備ができました。
sum関数の適用(56-57行目、
goto:ptr_sum
から:ptr_sum_ret
の前まで)
いよいよsum関数を適用します。
goto:ptr_sum
の直前の状態は以下のようになっています。
ここでスタックトップの部分のみに着目します。
さて、sum関数を開始する時のスタックの状態は、次の状態になっている必要があるのでした。
これらの画像を比較すると、
- sum関数の入力、・・・、が、それぞれ、、・・・、に対応
- sum関数の入力(=配列の長さ)が、に対応
と対応することがわかります。
したがって、現在のスタックの状態でsum関数を実行すると、
スタックトップから個の要素、すなわち、、・・・、の合計が計算され、
その結果がスタックトップにプッシュされます。
(正確には、「プッシュされた状態になります。」)
したがって、goto:ptr_sum
が実行され、処理が終わると、
スタックが次の状態になって:ptr_sum_ret
に戻ってきます。
この時のは、メモリ1からメモリm-1までのメモリ要素数の総和です。
つまり、
です。
スタックの原状復帰(58-67行目、
:ptr_sum_ret
のあとから:ptr_add_ofs
の前まで)
メモリ要素数の総和が、「ほぼ」計算できたので、スタックの状態を元に戻します。
まずは計算結果を退避します。
計算結果は最終的にはスタックトップの付近に来るようにしたいので、
黄色のの直上にを移動させます。
幸運なことに、スタックトップから黄色のまでの深さは、
スタックトップ付近にある値から計算することができます。
そこで、を複製しながら、スタックトップを次の状態にして、
roll命令を実行します。
これで、がの直上に来ました。
次に、最初に埋め込んでおいたピンク色のとを、
の下からスタックトップに移動させます。
ピンク色のとまでの位置も、先ほどと同様にから計算できます。
今回は2つ値を掘り起こすため、スタックトップの値は負数「-2」とします。
この状態でrollを実行すれば、ピンク色のとがスタックトップにきます。
そしていよいよ、スタックの原状復帰です。
とから、rollの引数を計算し、
rollを実行すると、
分離していた、メモリの要素数の配列、・・・、と、
、・・・、が再び連結されます。
同時に、sum関数の出力結果が、スタックトップに現れます。
これで、今回の関数の峠は越えました。
の計算仕上げ(68-70行目、
:ptr_add_ofs
から:ptr_dup_N
の前まで)
さて、今のは、まだ本当に求めたいではありません。
今のの値は次の通りです。
一方で本当に求めたいの値は以下の通りです。
つまり、まだが足されていません。
はスタックトップ付近にあるので、この補正はそんなに難しくありません。
スタックトップにを複製して、
add命令を実行します。
これでの計算は完了しました。
戻り値の生成(71-77行目、
:ptr_dup_N
からgoto:ptr_end
まで)
大詰めです。
今回の関数の戻り値はだけではありませんでした。
この図のように、、、を追加する必要があります。
それぞれの意味は、過去記事をご参照ください。
ymos-hobby-programing.hatenablog.com
ymos-hobby-programing.hatenablog.com
これらの追加は難しくありません。
、については、スタックにある値をコピーすればOKです。
は、「処理がうまく行ったぜぇ!!」ということを表すために
定数1とするだけなので、素直に1をプッシュします。
そして、やっと、ようやく、:ptr_end
に飛んで、
全ての操作が終了です。
お疲れ様です!!
補足:エラー処理(79-81行目、
:ptr_err
からgoto:ptr_end
まで)
最後にエラー処理について補足します。
エラー処理は79-81行目で実施します。
21行目、または、35行目で、メモリ範囲外にアクセスしようとした時に、
ここに飛んできます。
飛んできた時のスタックの状態は次の通りです。
出力時の状態は、次の通りですので、
、、、を足す必要があります。
仕様上、エラーが発生した場合はこれらの値は全て0となるので、
素直に0を4回プッシュします。
なお、0は直接プッシュできないので、適当な値(ここでは1)をプッシュして複製し、
それらの差をとって0として、3回複製していきます。
出力(153行目から最後まで、
:ptr_end
以降)
最後に、スタックから値を4つ取り出して表示しています。
半角スペース区切りで出力するため、値をひとつ出力するごとに、
スタックに半角スペースの文字コード()を作り、出力しています。
スタックトップから出力されるため、出力順は、
となります。
最後に
書いたよ。書き上げたよ。めっちゃ疲れたわ。
いつも思うんですが、こんな説明で大丈夫だろうか?
まあなんとなく雰囲気を掴んでもらえればよしとするか。
今回の解説ですが、ちょっと難儀したので、
次回以降、何をどう説明するかを考えないといけないなぁと思います。
あと、定期的に更新しないとなあ、と反省仕切りの今日この頃でした。
もうちょっと負荷の少ない書き方しないと続かんね、やっぱり。
では。