memomimium

まあいつものサンプルを考える

fn fbdelay(input:float,fb:float,dtime:float)->float{
    return input + delay(self,dtime)*fb
}
 
fn twodelay (input:float,dtime:float)->float{
    return fbdelay(input,dtime,0.7)+fbdelay(input,dtime*2,0.8)
}
 
fn dsp (input:float)->float{
    return twodelay(input,400)+twodelay(input,800)
}
 

コールツリーとしては

 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バイトは現在のリングバッファの位置、残りをメモリという扱いにすれば良い。ただメモリのサイズを決定すんのがなあ〜

fn fbdelay(input:float,fb:float,dtime:float)->float{
    return input + delay(self,dtime)*fb
}
 
fn twodelay (input:float,dtime:float)->float{
    return fbdelay(input,dtime,0.7)+fbdelay(input,dtime*2,0.8)
}
 
fn dsp (input:float)->float{
    return twodelay(input,400)+twodelay(input,800)
}
 

だめかも

関数型を受け取って関数を返すfilterbank的な例がこれだとダメなことに気がついた。少なくともリニアでフラットな内部状態ストレージでは実現無理。

fn onepole(x,g){
	x*(1.0-g) + self*g
}
fn filterbank(n,filter){
	let next = filterbank(n-1,filter);
	if (n>0){
		|x,freq| filter(x,freq+n*100) +next(x,freq)
	}else{
		|x,freq| filter(x,freq+n) 
	}
	
}
let myfilter = filterbank(3,onepole)

みたいな感じだとするとこうで

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が使える想定として

call_function<is_tailcall,is_stateful>(&mut self,f:F){
	ifc !is_tailcall{
		self.stack.push_return_address();
	}
	ifc is_closure{
		self.stateptr_stack.push(f.state_stack_ptr)
	}
	let nret = f(self);
		ifc is_closure{
		self.stateptr_stack.pop()
	}
	ifc !is_tailcall{
		self.stack.push_return_address();
	}
}