まあいつものサンプルを考える
コールツリーとしては
dsp
| |
twodelay(1) twodelay(2)
| | | |
fbdelay(1) fbdelay(2) fbdelay(3) fbdelay(4)
| | | |
delay,feed, delay,feed delay,feed delay,feed
-
実行中のVMから見れば、簡約されたあとにfeedの項が置き換えられて出てくるわけではない
-
関数の定義にジャンプしてみて初めてfeedが現れる
-
しかし、そのfeedがどこの情報を保持しているかは呼び出し元の情報が必要
-
コール元の情報を関数の引数として渡す、というのが前とったやり方
-
feedのコールツリーを持つやり方はポインタを辿ってく形になるのであんまやりたくない
-
VMがクロージャをsetUpvalue/getUpvalueで解決してるような感じで解決できないか
-
callの命令は今call、callCls、callExtの3種類存在
- これはVM上の参照するテーブルが違うので必要
-
例えば、VMマシン側に、
feed_baseptr
みたいな情報を持っておく- callのオペランドの中にこのbaseptrからのオフセットバイト数を含めるようにする
- いや、コンパイル時、call命令の前にsetFeedAddressみたいな命令を差し込めばそれでいいのか
- で、delay命令が来たときは現在のアドレスから必要な長さをリングバッファとして使用する
- getfeed命令が来たときは現在のアドレスからその型分のバイト数を取り出して、、、どうする?
- 即値用の、getfeedfloatと、それ以外のgetfeedptrは分けたほうがいいな
- call命令、delay、getfeed命令が終わるたびにbaseptrの場所は戻される
- いや、最後にsetfeedしなきゃいけないからself用のbaseptrに戻す必要があるのかな
- 場合によってはdelayの結構長いタイム分をオフセットしないといけないわけだから、命令長が足りなくならんかこれ
- オフセットの値を即値でやるのと、レジスタからロードする2パターンの命令持っておけば良い
まあこんな感じで行けそう
state_size:
fn fbdelay(input,fb,dtime){
// reg: input,fb,dtime
getfeedfloat // load feed to reg3
pushfeedoffset 8 // shift feed_base by 64bit
delay 3 2 4// write feed and pop head from dtime address offset, push to reg4
mulF 4 2 5// multiplay result of delay(reg4) and fb(reg2)
addF 0 5 6
popfeedoffset 8 // shift feed_base by 64bit
setfeed 6
return 6
}
fn twodelay (input,dtime){
// reg:input,dtime
movConst 'fbdelay' 2
mov 0 3
mov 1 4
movConst '0.7' 5
call 2 3 1 //reg2 is result of fbdelay
mov 1 3
movConst '2.0' 4
mulF 3 4 5
mov 5 3
movConst 'fbdelay' 4
mov 0 5
mov 3 6
movConst '0.8' 7
pushfeedoffset 8
call 4 3 1 //reg4 is result of second fbdelay
popfeedoffset
addF 2 4 5
return 5
}
fn dsp (input){
movConst 'twodelay' 1
mov 0 2
movConst '400' 3
call 1 2 1 //reg1 is result of twodelay
movConst 'twodelay' 2
mov 0 3
movConst '800' 4
pushfeedoffset 8
call 2 2 1
addf 1 2 3
return 3
}
delayはどうしておくかというと、最初の8バイトは現在のリングバッファの位置、残りをメモリという扱いにすれば良い。ただメモリのサイズを決定すんのがなあ〜
だめかも
関数型を受け取って関数を返すfilterbank
的な例がこれだとダメなことに気がついた。少なくともリニアでフラットな内部状態ストレージでは実現無理。
みたいな感じだとするとこうで
fn onepole(x,g){
movc 2 "1"
mov 3 1
sub 2 2 3
mul 2 0 2
mov 3 1
getstate 4
mul 3 3 4
add 3 2 3
}
fn lambda_true(x,freq){ // x:0 freq:1
getupvalue 2 0 //get n
movc 3 "100"
mul 2 2 3
add 1 1 2
getupvalue 2 1 //get filter
mov 4 0
mov 3 1
callcls 2 2 1
getupvalue 3 2 //get next
mov 4 0
mov 5 1
callcls 3 2 1
movc 3 "1"
add 3 3 4
ret 3 1
}
fn lambda_false(x,freq){// x:0 freq:1
getupvalue 2 0 //get n
add 2 2 1
getupvalue 3 1 //get filter
mov 4 0
mov 5 2
callcls 3 2 1
ret 3 1
}
fn filterbank (n,filter){ //n:0 filter: 1
movc 2 "-1"
sub 2 0 2
mov 3 "filterbank"
mov 4 1
mov 5 2
call "filterbank" 2 1 //now stack 2 is a closure "next"
mov 3 0
movc 4 "0"
eq 3 3 4
jmpifneg 3 :_else:
closure 3 "lambda_true"
jmp :return:
:else:
closure 3 "lambda_false"
:return:
ret 3 1
}
あ、でもcallcls
命令実行の時だけ、最初にgetstateを実行、暗黙的に読み出して、その中身のポインタへ飛べばいいのか
Closureを作った時にどうやってstateのメモリを拡張するかというと、ヒープ上のクロージャのデータ構造自体にstateへのポインタを持たせないと無理?
最小限の表現としてはCall(Stateポインタをいじる可能性がある)とCallClsの2種類で対応可能なんだろうけど、最適化を考えると
- Call (ほぼ純粋な関数)
- CallState (後々SelfとかDelayを呼び出しうる関数)
- CallClosure
- CallExtFunction
ここにTailCallとか加えると、それぞれに一つずつTailCall用の命令が映えるみたいな感じになるのだろうか
コンパイル時ifが使える想定として