memo mimium

Implement multi-stage computation intepreter by tomoyanonymous · Pull Request #136 · mimium-org/mimium-rs · GitHub

シンプルに構文木レベルでのインタプリタを別途作ることで実現できそう。

課題

  • 組み込みの関数の型情報がマクロ用(レベル0)とVM用(レベル1)で分かれているので、両方にあることを別々に宣言しなければならない
    • 逆に、ベクターのappend・removeや文字列操作など、メモリを確保する操作はレベル0でのみ利用できる組み込み関数として制限すると、便利かもしれない
    • 逆に、selfdelayはレベル1でのみ利用できる関数とする
    • レベル0,1両方で利用できるpersistentなモジュールは、上記2種類のいずれも使わず、かつescapeとBracketも利用しないものであればよい、ということになる
  • もともとディレイの最大サイズはリテラルで指定しなければいけないという問題があったが、これをmake_delay(size:float)-> <(float,float)->float>(レベル0で最大時間を指定すると、入力と遅延時間をとる関数のコードを返すという関数)にできるかも
fn fbdelay(max_time){
  `|input,time,fb| {
    (input + self*fb, time) |> make_delay!(max_time)
   }
}
  • こういうfbdelayを2個以上つかうときのコードはどうなるのかな
    • というか、fbdelayで生成されたコードをMIRに持ってくときにどうなるのか?
    • Value型を単なるCodeじゃなくてDelayとして特別な値にリダクションすればいいのかな

otopoiesisでパラメーターを生成するのにも使える?

fn synth(freq,gate){
  osc(freq)*gate
}
 
fn synth_module(freq:()->float,gate:()->float){
 `||{ synth(freq(),gate()) }
}

これのモジュールを評価すると、freq,gateがUIに現れるという感じでできるのかな(そして、ここでUIの範囲制限をするためにも数値型が範囲を持っていた方がいいということになりそう)

うーん、サンクを手動で使わず表現できるような何かが欲しいなあ

fn wrap_module(param:Param, synth:(Param)->float ){
  `{ | | synth(param |> invoke) }
}

Param型はinvokeでuiからの値を取れる、サンクをアンラップするようなメソッドを持つ型クラスに属している、という感じで、ジェネリクスが実装出来たらいけそうね

マクロを提供するプラグイン

プラグインも、各関数が返す型と同時にどのレベルでその関数が使えるかをコンパイラに渡す必要がある

ファイル読み込みはどうなる?

mimiumのファイルIOの話。マクロの展開より先に型の評価が行われるので、例えばファイルを読み込んでからチャンネル数が分かる、みたいな場合型情報を読み込む段階でファイルを読みこまなければならない、がその評価のためにはマクロを実行して文字列の値を受け取らなければならない、、、ということで、結局依存型がないと多段階計算でも対応できない。

結局、読み込んで返す値として次元数の値を持つ配列を返すしかないのかも

ただ、よく考えるとMaxのsfplayとかだって、ファイルのチャンネル数とは別に出力のチャンネル数先に指定するしな(入ってるファイルの中身のCh数に応じて処理を分岐させるというユースケースがあんまり思いつかない)

シンタックス

色んな流派があるが、バッククォートでクォート(コード型の値を作る)、$でスプライス(コードを埋め込む)ようにする。

let x = `{1+2} //コード/クォートのシンタックス
${x} //エスケープ/スプライスのシンタックス
//or
$x
//or
$(x)

どちらも単なる単項演算子として定義しているので、複雑な式の場合は()でグループ化するか、{}でブロックを生成すればよろしい。

型シグネチャ

新たにCode型が導入されるので、これもバッククォート演算子を導入することにする。

let x : `(float)->float //これは関数型に対する宣言
let y : (float)->`float //これは返り値がCode型の場合

レコードとタプル型で曖昧性が出そうな気もするけど、多分大丈夫

シンタックスシュガー

ファイル内でステージを変えたブロックを作りたい場合、ブロックがどんどんネストされていって面倒。

あと、mimiumではブロック${} の内側にfnでの関数宣言ができないのもダルい。

ステージ表記については、一旦0をmacro、1をmainと呼ぶことにする。それ以上にステージが増える可能性もあるけど今はこれで十分

//ステージ1からスタート
#stage(macro)
//ここはステージ0
let x = {...}
 
#stage(main)
//ここで戻る
let y = (x+...)
 

これをこういう形でネストした式へ変形

`{//コンパイラはステージ0からスタートするためにCodeを追加
//ステージ1からスタート
${//#stage(macro) start
//ここはステージ0
   let x = {...}
 `{//#stage(main) start
 //ここで1に戻る
 let y = (x+...)
 }//#stage(main) end
 
  }//#stage(macro) end
}// main code end

この、最後に閉じ括弧が溜まっていく形にしない限り、yはxを参照できなくなってしまう

最終的に、ステージはモジュール単位で変更していくほうが都合がよさそう。

とはいえ、簡易的に1ファイル内でステージ0、1の変更もしたいかな

persistentな宣言をした時の意味論はどうしよう(コードを複製する?)

persistentである時点でcode型が現れないかのチェックをする必要はある

単なる置き換えだと、includeしたファイルの定義によってその下のステージが変わってしまう可能性がある?