memo mimium

(v3で導入済み。)

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

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

構文の拡張

Type

Code Type を加える

\begin{align}<!-- line:19 --> \tau ::=<!-- line:20 --> &\quad R \quad &\\<!-- line:21 --> |&\quad I_n \quad &n \in \mathbb{N} \\<!-- line:22 --> |&\quad \tau → \tau &\\<!-- line:23 --> |&\quad \langle \tau \rangle<!-- line:24 --> \end{align}<!-- line:25 --> $$<!-- line:26 --> <!-- line:27 --> ### Expression<!-- line:28 --> <!-- line:29 --> $$<!-- line:30 --> \begin{align}<!-- line:31 --> e ::= &\quad R & R \in \mathbb{R} [number]&\\<!-- line:32 --> |&\quad \lambda x.e& [abs]&\\<!-- line:33 --> |&\quad let\; x\; =\; e\; in\; e& [let]&\\<!-- line:34 --> |&\quad fix\; x.e & [fixpoint]&\\<!-- line:35 --> |&\quad e \ e \quad& [app]&\\<!-- line:36 --> |&\quad if\; (e_c) \; e_t\; else\; e_e & [if] &\\<!-- line:37 --> |&\quad delay(x,e_{time},v_{bound})&[delay]&\\<!-- line:38 --> |&\quad mem(e) &[mem]&\\<!-- line:39 --> |&\quad feed\ x.e &[feed]&\\<!-- line:40 --> |&\quad `(e) &[quote]&\\<!-- line:41 --> |&\quad $(e) &[splice]&\\\<!-- line:42 --> |& \ ...<!-- line:43 --> \end{align}<!-- line:44 --> $$<!-- line:45 --> <!-- line:46 --> ### Value<!-- line:47 --> <!-- line:48 --> $$<!-- line:49 --> \begin{align}<!-- line:50 --> v ::=<!-- line:51 --> |&\quad R & R \in \mathbb{R} [number]&\\<!-- line:52 --> |&\quad cls(e,E) &[closure]&\\<!-- line:53 --> |&\quad \langle v \rangle &[code]&\\<!-- line:54 --> \end{align}<!-- line:55 --> $$<!-- line:56 --> <!-- line:57 --> <!-- line:58 --> ## 課題<!-- line:59 --> <!-- line:60 --> - 組み込みの関数の型情報がマクロ用(レベル0)とVM用(レベル1)で分かれているので、両方にあることを別々に宣言しなければならない<!-- line:61 --> - 逆に、ベクターのappend・removeや文字列操作など、メモリを確保する操作はレベル0でのみ利用できる組み込み関数として制限すると、便利かもしれない<!-- line:62 --> - 逆に、`self`や`delay`はレベル1でのみ利用できる関数とする<!-- line:63 --> - レベル0,1両方で利用できるpersistentなモジュールは、上記2種類のいずれも使わず、かつescapeとBracketも利用しないものであればよい、ということになる<!-- line:64 --> - もともとディレイの最大サイズはリテラルで指定しなければいけないという問題があったが、これを`make_delay(size:float)-> <(float,float)->float>`(レベル0で最大時間を指定すると、入力と遅延時間をとる関数のコードを返すという関数)にできるかも<!-- line:65 --> - ただ、これやるとディレイが絡む関数はすべてレベル0定義になるのかな<!-- line:66 --> - なんかそれよりは、[[数値プリミティブ型に常に範囲をつける]]とかのほうが筋がいいかも<!-- line:67 --> <!-- line:68 --> ```rust fn fbdelay(max_time){<!-- line:70 --> `|input,time,fb| {<!-- line:71 --> (input + self*fb, time) |> make_delay!(max_time)<!-- line:72 --> }<!-- line:73 --> }<!-- line:74 --> ``` <!-- line:76 --> - こういうfbdelayを2個以上つかうときのコードはどうなるのかな<!-- line:77 --> - というか、fbdelayで生成されたコードをMIRに持ってくときにどうなるのか?<!-- line:78 --> - Value型を単なるCodeじゃなくてDelayとして特別な値にリダクションすればいいのかな<!-- line:79 --> <!-- line:80 --> [[otopoiesis]]でパラメーターを生成するのにも使える?<!-- line:81 --> <!-- line:82 --> ```rust fn synth(freq,gate){<!-- line:84 --> osc(freq)*gate<!-- line:85 --> }<!-- line:86 --> <!-- line:87 --> fn synth_module(freq:()->float,gate:()->float){<!-- line:88 --> `||{ synth(freq(),gate()) }<!-- line:89 --> }<!-- line:90 --> ``` <!-- line:92 --> これのモジュールを評価すると、freq,gateがUIに現れるという感じでできるのかな(そして、ここでUIの範囲制限をするためにも数値型が範囲を持っていた方がいいということになりそう)<!-- line:93 --> <!-- line:94 --> うーん、サンクを手動で使わず表現できるような何かが欲しいなあ<!-- line:95 --> <!-- line:96 --> ```rust fn wrap_module(param:Param, synth:(Param)->float ){<!-- line:98 --> `{ | | synth(param |> invoke) }<!-- line:99 --> }<!-- line:100 --> ``` Param型はinvokeでuiからの値を取れる、サンクをアンラップするようなメソッドを持つ型クラスに属している、という感じで、ジェネリクスが実装出来たらいけそうね<!-- line:102 --> <!-- line:103 --> <!-- line:104 --> ## マクロを提供するプラグイン<!-- line:105 --> <!-- line:106 --> プラグインも、各関数が返す型と同時にどのレベルでその関数が使えるかをコンパイラに渡す必要がある<!-- line:107 --> ### ファイル読み込みはどうなる?<!-- line:108 --> <!-- line:109 --> [[mimiumのファイルIO]]の話。マクロの展開より先に型の評価が行われるので、例えばファイルを読み込んでからチャンネル数が分かる、みたいな場合型情報を読み込む段階でファイルを読みこまなければならない、がその評価のためにはマクロを実行して文字列の値を受け取らなければならない、、、ということで、結局[[依存型]]がないと多段階計算でも対応できない。<!-- line:110 --> <!-- line:111 --> 結局、読み込んで返す値として次元数の値を持つ配列を返すしかないのかも<!-- line:112 --> <!-- line:113 --> ただ、よく考えると[[Max]]のsfplayとかだって、ファイルのチャンネル数とは別に出力のチャンネル数先に指定するしな(入ってるファイルの中身のCh数に応じて処理を分岐させるというユースケースがあんまり思いつかない)<!-- line:114 --> <!-- line:115 --> ## シンタックス<!-- line:116 --> <!-- line:117 --> 色んな流派があるが、バッククォートでクォート(コード型の値を作る)、$でスプライス(コードを埋め込む)ようにする。<!-- line:118 --> <!-- line:119 --> ```rust let x = `{1+2} //コード/クォートのシンタックス<!-- line:121 --> ${x} //エスケープ/スプライスのシンタックス<!-- line:122 --> //or<!-- line:123 --> $x<!-- line:124 --> //or<!-- line:125 --> $(x)<!-- line:126 --> ``` <!-- line:128 --> どちらも単なる単項演算子として定義しているので、複雑な式の場合は`()`でグループ化するか、`{}`でブロックを生成すればよろしい。<!-- line:129 --> <!-- line:130 --> ### 型シグネチャ<!-- line:131 --> <!-- line:132 --> 新たにCode型が導入されるので、これもバッククォート演算子を導入することにする。<!-- line:133 --> <!-- line:134 --> ```rust let x : `(float)->float //これは関数型に対する宣言<!-- line:136 --> let y : (float)->`float //これは返り値がCode型の場合<!-- line:137 --> ``` <!-- line:139 --> レコードとタプル型で曖昧性が出そうな気もするけど、多分大丈夫<!-- line:140 --> <!-- line:141 --> ### シンタックスシュガー<!-- line:142 --> <!-- line:143 --> ファイル内でステージを変えたブロックを作りたい場合、ブロックがどんどんネストされていって面倒。<!-- line:144 --> <!-- line:145 --> あと、mimiumではブロック`${}` の内側に`fn`での関数宣言ができないのもダルい。<!-- line:146 --> <!-- line:147 --> ステージ表記については、一旦0をmacro、1をmainと呼ぶことにする。それ以上にステージが増える可能性もあるけど今はこれで十分<!-- line:148 --> <!-- line:149 --> ```rust //ステージ1からスタート<!-- line:151 --> #stage(macro)<!-- line:152 --> //ここはステージ0<!-- line:153 --> let x = {...}<!-- line:154 --> <!-- line:155 --> #stage(main)<!-- line:156 --> //ここで戻る<!-- line:157 --> let y = (x+...)<!-- line:158 --> <!-- line:159 --> ``` <!-- line:161 --> これをこういう形でネストした式へ変形<!-- line:162 --> ```rust `{//コンパイラはステージ0からスタートするためにCodeを追加<!-- line:164 --> //ステージ1からスタート<!-- line:165 --> ${//#stage(macro) start<!-- line:166 --> //ここはステージ0<!-- line:167 --> let x = {...}<!-- line:168 --> `{//#stage(main) start<!-- line:169 --> //ここで1に戻る<!-- line:170 -->  let y = (x+...)<!-- line:171 -->  }//#stage(main) end<!-- line:172 --> <!-- line:173 --> }//#stage(macro) end<!-- line:174 --> }// main code end<!-- line:175 --> ``` <!-- line:177 --> この、最後に閉じ括弧が溜まっていく形にしない限り、yはxを参照できなくなってしまう<!-- line:178 --> <!-- line:179 --> 最終的に、ステージは[[mimiumのモジュールシステム|モジュール]]単位で変更していくほうが都合がよさそう。<!-- line:180 --> <!-- line:181 --> とはいえ、簡易的に1ファイル内でステージ0、1の変更もしたいかな<!-- line:182 --> <!-- line:183 --> persistentな宣言をした時の意味論はどうしよう(コードを複製する?)<!-- line:184 --> <!-- line:185 --> persistentである時点でcode型が現れないかのチェックをする必要はある<!-- line:186 --> <!-- line:187 --> 単なる置き換えだと、includeしたファイルの定義によってその下のステージが変わってしまう可能性がある?<!-- line:188 --> <!-- line:189 --> <!-- line:190 --> --- <!-- line:192 --> [[OCaml]]で書いてみてる<!-- line:193 --> <!-- line:194 --> ```ocaml type expr = NumLit of float<!-- line:196 --> | Var of string<!-- line:197 --> | App of expr * expr<!-- line:198 --> | Lam of string * expr<!-- line:199 --> | Delay of int * expr * expr<!-- line:200 --> | Feed of string * expr<!-- line:201 --> | Quote of expr<!-- line:202 --> | Splice of expr<!-- line:203 --> | Tuple of expr list<!-- line:204 --> | Proj of expr * int<!-- line:205 --> type bound = string * value<!-- line:206 --> and env = bound list<!-- line:207 --> and value = Real of float<!-- line:208 --> | OpenFn of string * expr * int<!-- line:209 --> | Closure of string * expr * env<!-- line:210 --> | Code of expr<!-- line:211 --> type state = float list<!-- line:212 --> type stage = int<!-- line:213 --> type stateptr = int<!-- line:214 --> <!-- line:215 --> let lookup: string->env->value =<!-- line:216 --> fun name env -> let finder = fun (n,v) -> n == name in<!-- line:217 --> let (_n,v) = List.find finder env in<!-- line:218 --> v<!-- line:219 --> <!-- line:220 --> <!-- line:221 --> <!-- line:222 --> let rec contain_freevars: string -> expr -> env -> bool = <!-- line:223 --> ...<!-- line:224 --> <!-- line:225 --> <!-- line:226 --> let rec eval : stage -> expr -> env ->value = <!-- line:227 --> fun stage e env -> match e with<!-- line:228 --> | NumLit(n) -> Real(n)<!-- line:229 --> | Var(name)-> lookup name env<!-- line:230 --> |<!-- line:231 --> |_ -> raise (Failure "not implemented yet")<!-- line:232 --> <!-- line:233 --> ```