--- date: "2023-09-20T17:12:17+0900" --- #memo #mimium #programming-language [[音楽プログラミング言語の形式化#mimium と 多段階計算]] [[多段階計算]]を取り入れたい → [[多段階計算を命令型VMインストラクションで表現したい]] とりあえず[[The w-calculus a synchronous framework for the verified modelling of digital signal processing algorithms|W Calculus]]を自然に拡張してみる。 $W$ Calculusとmimiumの形式は似ているが、主に2つの違いがある。 1. $W$ Calculus はLinear-Time Invariant なシステムを想定しているため、基本演算は項の加算と、項と定数の乗算しか使えない。 2. $W$ Calculusでは関数が数をとって数を返すものしかない。つまり、関数やfeedの項を取ったり返すような高階関数は想定されていない。 問題になるのは後者の方だ。 ## 型 n以下の自然数$I_n$ (ディレイのbounded access用) $$ \begin{align} \tau ::=&\quad R_a \quad & a \in \mathbb{N}\\ |&\quad I_n \quad &n \in \mathbb{N} \\\ |&\quad \tau → \tau \quad &a,b \in \mathbb{N}\\ % |&\quad \langle \tau \rangle \end{align} $$ とりあえず1要素のタプルと普通のRは区別しないことにする (そしてよく見るとこれは関数→関数のような高階関数を許してないんだな) そうか高階関数を考えなければクロージャを考慮する必要もないものな [[ブロックとサンプルの互換性]]をどうするかが問題? ## 値 一旦タプルについては考えないことにしよう $$ \begin{align} v \; ::= & \quad R \\ | & \quad \lambda x:\tau.e \quad & [lambda]\\ |& \quad feed \; x.e \quad & [feed] \\ \end{align} $$ ## 項 $$ \begin{align} e \; ::=& \quad x \quad x \in \mathbb{V} \quad & [value]\\ |& \quad \lambda x.e \quad & [lambda]\\ |& \quad e \; e \quad & [app]\\ |& \quad fix \; x.e \quad & [fixpoint]\\ |& \quad feed \; x.e \quad & [feed] \\ |& \quad delay \; e \; e & [delay]\\ \end{align} $$ 基本演算(Intrinsic)は直感に任せる 本来はfixの中でfeedを使ったり、feedの中でfixを使うとエラーだが、結局シンタックスレベルでは排除できないので型でエラーとして弾くことにする…? いや値レベルでの切り分けは不可能なので、こうする ## 実例 ```rust fn cascade(order:int,fb)->(float->float){ if(order>0){ |x|{ cascade(order-1)(x) *(1-fb) + self*fb } }else{ |x| x } } ``` ちょっとわかりやすさのために`self`を使わずfeedにしてみる ```rust fn cascade(order:int,fb)->(float->float){ if(order>0){ |x|{ feed(y) { cascade(order-1)(x) *(1-fb) + y*fb } } }else{ |x| x } } ``` あー、今までは`fn(x)`でself使うものを`feed(self).lambda(x).e,`って感じに自動的に変換してたけど、変換するとしたら`lambda(x).feed(x).e`の方が良かったってことなんだな これを`cascade(3,0.9)`とかで簡約してみるか ```rust cascade(3,0.9) |x|{ let res = cascade(2)(x); feed(y) { res*0.1 + y*0.9 } } |x1|{ let res1 =|x2|{ let res2 = cascade(1)(x2); feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ let res2 = |x3|{ let res3 = cascade(0)(x3); feed(y3) { res3*0.1 + y3*0.9 } }(x2); feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ let res2 = |x3|{ let res3 = |x|{x}(x3); feed(y3) { res3*0.1 + y3*0.9 } }(x2); feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ let res2 = |x3|{ let res3 = x3; feed(y3) { res3*0.1 + y3*0.9 } }(x2); feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ let res2 = |x3|{ feed(y3) { x3*0.1 + y3*0.9 } }(x2); feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ let res2 = feed(y3) { x2*0.1 + y3*0.9 }; feed(y2) { res2*0.1 + y2*0.9 } }(x1); feed(y1) { res1*0.1 + y1*0.9 } } |x1|{ let res1 =|x2|{ feed(y2) { feed(y3) { x2*0.1 + y3*0.9 } *0.1 + y2*0.9 } }(x1); feed(y1) { res1 *0.1 + y1*0.9 } } |x1|{ let res1 = feed(y2) { feed(y3) { x1 *0.1 + y3*0.9 } *0.1 + y2*0.9 }; feed(y1) { res1 *0.1 + y1*0.9 } } |x1|{ feed(y1) { feed(y2) { feed(y3) { x1*0.1 + y3*0.9} *0.1 + y2*0.9 }*0.1 + y1*0.9 } } ``` feedの項に対する加算とか乗算の計算は簡約がしづらいなあ 時間0の時のyは全て0として、 ```rust |x1,y1_ref,y2_ref,y3_ref| { let y1 = *y1_ref; let y1_next= { let y2 = *y2_ref; let y2_next = { let y3 = *y3_ref; let y3_next = x1*0.1 + y3*0.9; *y3_ref = y3_next; y3_next }*0.1 + y2*0.9; *y2_ref = y2_next; y2_next }*0.1 + y1*0.9; *y1_ref = y1_next; y1_next } ``` やっぱdenotationalの方が定義しやすいかもなあ ああでもfeedを無事に展開できるということは、feedの項に対して`Cell`を割り当てることそのものには間違いはないのか ただ、例えばFeedの項の中に関数が残っちゃうような可能性もあるため、Feedのhistoryの中にLambdaの項が保存されるような状況が回避できない。 型付規則の中でfeed x.eのeがプリミティブというか、Boxedにならないものしか取れないようにすればいいのかね。そうするとValueはCopyトレイトを実装できて、feedの中に実際にはラムダが入ってたとしても、簡約後は必ずValueになっていると ## 型(改正版) というわけで型の定義再訪 $$ \begin{align} \tau_p ::=&\quad R_a \quad & a \in \mathbb{N}\\ |&\quad I_n \quad &n \in \mathbb{N} \\ \tau :: = &\quad \tau_p\\ |&\quad \tau → \tau \quad &a,b \in \mathbb{N}\\ % |&\quad \langle \tau \rangle \end{align} $$ でラムダ抽象とFeedの型付け規則こういう感じになると $$ \frac{\Gamma, x:\tau^a \vdash e:\tau^b}{\Gamma \vdash \lambda x.e:\tau^a \to \tau^b } $$ $$ \frac{\Gamma, x : \tau_p^a \vdash e: \tau_p^a }{\Gamma \vdash feed\ x.e:\tau_p^a} $$ タプルとかレコードもできるけど、関数をタプルの要素にしたりはできない(できないでもないけど、「そういう型をとれるタプル」と「そういうのできないタプル」を分けて考える必要がある)、って感じでユーザーにはややこしいですねえ ## Feedのスコープと簡約の順番について ところでさっきの`cascade`関数ってさ、わざわざ高階関数にせず一括でやれるんですかね、 ```rust fn cascade_f(order:int,fb,x)->float{ letrec cascade = if(order>0){ |x|{ cascade(N-1)(x) *(1-fb) + self*fb } }else{ |x| x } cascade(x) } let res = cascade_f(3,0.9,input) ``` ともあれコピーキャプチャのクロージャでも問題はなさそうだけども、この状態だと`cascade`のfeedのコンテキストは毎サンプル終了しちゃうって感じなんだよね ## VMのインストラクションとデータ構造 ```rust type Ref = u8; enum UpIndex{ Local(Reg), UpValue(u64) } struct FuncProto{ instructons: Vec, constants: Vec, upvalue_idxs: Vec, feed_idx: Vec } ``` この関数だけだとfeedidをどうつければいいかなあ ```rust fn filterbank(N,input,lowestfreq, margin,filter){ if(N>0){ let freq = lowestfreq+N*margin; return filter(input,freq) + filterbank(N-1,input,lowestfreq,margin,filter) }else{ return 0 } } fn lowpass(input,fb){ input* (1-fb) + self * fb } let lowpass = |input,fb|{feed(y) { input* (1-fb) + y * fb }} } let lowpass = |input,fb,ref_y|{ let res = input* (1-fb)+ deref(ref_y)*fb ref_y := res res } res = filterbank(3,input,100,2000,2.0,lowpass) ``` lowpassは最終的にlambda{feed{self}}的な感じになるが、それはfilterbankの中で呼ばれるまではわからない lowpassのバイトコードはこんな感じか ``` lowpass: //stack 0:input, 1: fb moveconst 2 0 sub 3 1 2 getfeed 4 mult 5 2 4 add 6 3 5 retfeed 6 1 const: 1_i64 upindexes: //nothing ``` ``` global_ftable: lowpass filterbank filterbank: //stack 0:N,1:input,2:lfreq,3:margin,4:filter moveconst 5 0 gt 6 0 5 jmpifneg 6 16 mult 6 0 3 add 7 2 0 move 6 6 7 move 7 4 // get filter move 8 1 move 9 6 callcls 7 2 1 //result on stack 7 moveconst 8 1 sub 9 0 8 move 10 -1 // get recursive call move 11 9 // prepare arguments... move 12 1 move 13 2 move 14 3 closure 15 4 // hof requires all function should be closure call 10 5 1 //recursive call, result on stack 10 add 0 7 10 jmp 1 moveconst 0 0 ret 0 1 const: 0_i64 -1_u64 ``` feedが呼ばれた時にどのfeedidかを判別するのはランタイム側の役目 --- ```rust fn cascade_f(order:int,fb,x)->float{ letrec cascade = if(order>0){ |x|{ cascade(N-1)(x) *(1-fb) + self*fb } }else{ |x| x } cascade(x) } fn phasor(freq)->float{ 1+self } fn doubleosc(freq)->freq{ phasor(freq)+phasor(freq+10) } let res = cascade_f(3,0.9,input)+doubleosc(440) ``` なんかこんな感じだとして、レキシカルに何番目の関数呼び出しか、 [[mimiumの中間表現を考える]]