[obsidian] vault backup: 2025-12-19 17:28:30[
All checks were successful
Build / build (push) Successful in 8m40s

This commit is contained in:
2025-12-19 17:28:30 +09:00
parent 233050d1fb
commit 34f1574108
2 changed files with 111 additions and 73 deletions

View File

@@ -83,4 +83,86 @@ $$
&\frac{E^n \vdash e_c \Downarrow n \quad n \leqq0\ E^n \vdash e_e\ \Downarrow v\ }{E^n \vdash\ if (e_c)\ e_t\ else\ e_t \Downarrow v }\ &\textrm{[E-IFFALSE]}\\
&\frac{E^n \vdash e_1 \Downarrow cls(\lambda x_c.e_c, E^n_c) E^n \vdash e_2 \Downarrow v_2\ E^n_c,\ x_c \mapsto v_2 \vdash e_c \Downarrow v }{E^n \vdash\ e_1\ e_2 \Downarrow v }\ &\textrm{[E-APP]}
\end{gathered}
$$
## 実用スモールステップ意味論
アイデア:クロージャ呼び出しとそれ以外の関数呼び出し(グローバルに登録されたものと即時呼び出しされるものとか)を分けよう
OCamlでの疑似コード未完成
```Ocaml
type expr = NumLit of float
| Var of string
| App of expr * expr
| Lam of string * expr
| Delay of int * expr * expr
| Feed of string * expr
| Quote of expr
| Splice of expr
| Tuple of expr list
| Proj of expr * int
type bound = string * value
and env = bound list
and value = Real of float
| OpenFn of string * expr * int
| Closure of string * expr * env
| Code of expr
type state = float list
type stage = int
type stateptr = int
let lookup: string->env->value =
fun name env -> let finder = fun (n,v) -> n == name in
let (_n,v) = List.find finder env in
v
let rec contain_freevars: string -> expr -> env -> bool =
...
let states = ... in
let stateptr = 0 in
let rec eval : stage -> expr -> env -> value =
fun stage e env -> match e with
| Var(name) ->
lookup env name
| Lam(x,e) ->
if contains_freevar(e) then OpenFn(x,e,statesize e)
else Closure(x,e,env)
| App(ef,ea) ->
let va = eval stage ea env in
let vf = eval stage ef env in
match vf with
| OpenFn(x,e,size)-> let r = eval stage e ((x,va)::env) in
shift stateptr size;
r
| Closure(x,e,cenv) -> eval stage e ((x,va)::cenv)
| Delay(n,et,ev) ->
let v = eval stage ev env in
let vt = eval stage et env in
update_ringbuf state stateptr v vt
| Feed(x,e)->
let size = statesize e in
let last = getstate state stateptr in
let current = eval stage e ((x,last)::env)
state[stateptr] := current;
last
| Quote(e)->
rebuild stage+1 e env
| Splice(e)->
let v = rebuild stage-1 e env
match v with
| Code(e) -> eval e env
_-> raise
and rebuild : stage -> expr -> env =
fun stage e env ->
if stage == 0 then eval e env
else Code(e)
```

View File

@@ -45,7 +45,9 @@ mimiumはコードを専用のVMバイトコードへコンパイルし実行す
コンパイラは、状態ストレージの読み出し位置ポインタを相対的に前後させる命令を適切に出力することで、VM実行時にはストレージの特定領域をフィードバックの状態変数やディレイ用のリングバッファとして解釈しデータを読み書きする。
過去のmimiumでは高階関数などを使うことによって任意の数のフィルタバンクのような、パラメトリックなプロセッサを生成することもできたが、こうしたプロセッサは状態ストレージのレイアウトとメモリサイズをコンパイル時に決定できなかった。そのため、グローバルな関数の呼び出しとクロージャ(実行時に高階関数から生成される関数)の呼び出しは区別され、クロージャのインスタンスに個別の状態ストレージを生成し、クロージャ呼び出し時に使用する状態ストレージそのものを切り替えることで対応していた
過去のmimiumでは高階関数などを使うことによって任意の数のオシレーターバンクのような、パラメトリックなプロセッサを生成することもできたが、こうしたプロセッサは本質的に状態ストレージのレイアウトとメモリサイズをコンパイル時に決定できない。これはif文などを通じて複数サイズの状態ストレージを操作する関数が一つの変数に縮約されうるためである
そのためmimiumでは、クロージャ実行時に高階関数から生成される関数と、そうでない自由変数をキャプチャしない関数の呼び出しを区別し、クロージャのインスタンスには個別の状態ストレージを生成し、クロージャ呼び出し時に使用する状態ストレージそのものを切り替えることで対応している。
今回提案するライブコーディング機能は、2つの機能追加によって実現される。
@@ -108,60 +110,7 @@ delay,mem,feedは信号処理用のプリミティブである。mem(e)はシン
また、多段階計算において重要な、`run`プリミティブを用いて一つ上のステージで定義されたものをその場で使用する越段階埋め込みCross-Stage Persistenceはmimiumでは実装されていない。一方、コンパイル時に計算した数値などをランタイムで使用するために、プリミティブをコード型へ持ち上げる`lift`関数はプリミティブとして用意されている。mimiumの型システムは現時点でジェネリクスを搭載していないため、各基本型毎に異なる名前の組み込み関数floatに対するlift_fなどが用意される。
### 操作的意味論
```ocaml
let states = ... in
let stateptr = 0 in
let rec eval : stage -> expr -> env -> value =
fun stage e env -> match e with
| Var(name) ->
lookup env name
| Lam(x,e) ->
if contains_freevar(e) then OpenFn(x,e,statesize e)
else Closure(x,e,env)
| App(ef,ea) ->
let va = eval stage ea env in
let vf = eval stage ef env in
match vf with
| OpenFn(x,e,size)-> let r = eval stage e ((x,va)::env) in
shift stateptr size;
r
| Closure(x,e,cenv) -> eval stage e ((x,va)::cenv)
| Delay(n,et,ev) ->
let v = eval stage ev env in
let vt = eval stage et env in
update_ringbuf state stateptr v vt
| Feed(x,e)->
let size = statesize e in
let last = getstate state stateptr in
let current = eval stage e ((x,last)::env)
state[stateptr] := current;
last
| Quote(e)->
rebuild stage+1 e env
| Splice(e)->
let v = rebuild stage-1 e env
match v with
| Code(e) -> eval e env
_-> raise
and rebuild : stage -> expr -> env =
fun stage e env ->
if stage == 0 then eval e env
else Code(e)
```
mimiumでは、
グローバルな関数呼び出しとそれ以外の呼び出しの区別。式に自由変数があるかないかを区別する必要がある。それを表示的意味論に落とし込めるのか
グローバルなラベル集合を定義して、グローバルラベルに基づく関数呼び出し、ラムダ項から直接の呼び出し
### シンタックスシュガー
#### シンタックスシュガー
mimiumでは、多段階計算の体系を直感的に扱えるように2つのシンタックスシュガーを導入している。
@@ -171,8 +120,11 @@ mimiumでは、多段階計算の体系を直感的に扱えるように2つの
### 多段階計算によるメタ操作の実例
```rust
以下、実際のmimiumのコードを用いて実例を解説する。以下で見せるコードはいずれもmimiumで実際に動作する、パラメトリックな加算合成シンセサイザーのコードである。
まず、Code.1は多段階計算を使わずに、再帰関数でクロージャを生成する例である。この例では、コードは一度コンパイルされ、ランタイムでグローバル環境が評価されるタイミングでadditive関数が実行され、myoscにクロージャが束縛される。この時、osc関数の内部状態はランタイムのクロージャインスタンスの上にそれぞれ確保される。
```rust
fn additive(n,gen){
let g = gen()
if (n>1){
@@ -182,9 +134,15 @@ fn additive(n,gen){
|rate| g(rate)
}
}
...
fn osc(){
...
let PI = 3.14159265359
fn phasor_shift (freq,phase_shift){
(self + freq/samplerate + phase_shift)%1.0
}
fn sinwave = (freq,phase){
phasor_shift(freq,phase)*2.0*PI |> sin
}
fn osc = (freq){
sinwave(freq,0.0) * 0.5
}
let myosc = additive(5, | | osc);
fn dsp(){
@@ -194,6 +152,8 @@ fn dsp(){
}
```
次に、Code.2が、多段階計算を用いたバージョンである。このコードは、シンタックスシュガーを外して純粋なクオートとスプライスをネストした表現に直すとCode.3のように展開される。ここで、additive関数はバイトコード生成の前の段階で、ステージ0で評価されCode.4のように展開される。
```rust
#stage(macro)
fn additive(n,gen){
@@ -207,14 +167,9 @@ fn additive(n,gen){
}
#stage(main)
let PI = 3.14159265359
fn phasor_shift (freq,phase_shift){
(self + freq/samplerate + phase_shift)%1.0
}
fn sinwave = (freq,phase){
phasor_shift(freq,phase)*2.0*PI |> sin
}
...
fn osc = (freq){
sinwave(freq,0.0) * 0.5
...//as same as non-macro version
}
fn dsp(){
let f = 200
@@ -223,7 +178,6 @@ fn dsp(){
}
```
```rust
`{ // after desugar
${
@@ -250,11 +204,13 @@ fn dsp(){
}
```
ステージ1で定義されているosc関数は、ステージ0でadditive関数の実行を通じてdsp関数の中に埋め込まれる。ここで、変数nは元々ステージ0で評価される変数のため、lift_fプリミティブ関数を用いてステージ1へ持ち越されている。
```rust
// after macro expansion
let osc = | | {
...
}
}
let dsp = | |{
let f = 200
let r = f |> |f| osc(f*5)/5 + osc(f*4)/4 + ... osc(f)
@@ -262,6 +218,8 @@ let dsp = | |{
}
```
マクロが展開された時点で、自由変数をキャプチャするクロージャはコードから消えていることがわかる。実行時にクロージャインスタンスは生成されないため、内部状態ストレージはグローバル環境に単なる配列としてアロケートすることが可能になる。
## コールツリーの解析
```rust
@@ -271,7 +229,7 @@ enum StateTree{
Feed(WordSize),
Mem(WordSize),
Delay(MaxTime),
DirectFnCall(Vec<StateTree>)
OpenFnCall(Vec<StateTree>)
}
```
@@ -321,7 +279,6 @@ fn osc(freq){
- phasor(freq)* 2 * PI |> sin
+ phasor(freq+(phasor(freq/10))) * 2 * PI |> sin
}
```
@@ -388,12 +345,11 @@ fn dsp(){//dspはFncall[Fncall[FnCall[Feed]],Fncall[Feed]]で変化なし
上のサンプルでは、はじめlfoを使って周波数をモジュレーションしている状態から、周波数は固定にして音量をモジュレーションする処理へと切り替えた例である。myfreq()とmyamp()はそれぞれどちらもosc関数を1度だけ呼び出すため、dsp関数の内部状態ツリーの構成は共通しており、再コンパイル時にデータが引き継がれる。
この時、myampにはmyfreqの最後の位相が引き継がれることになるが、今回実装しているphasor1はselfに保存される値が0~samplerate/freq、例えば1000Hzなら0~48の値のレンジを取り、これがmyampの中で使われているphasor2のselfのその値の本来のレンジは0~1であるべきにも関わらず引き継がれてしまう。
ただ、結局phasor2を実行したときには0~1のレンジに丸まるので大きな問題にはならない。ある関数がある範囲に収まることが保証されているということは、仮にそこで使われているselfに不正な値が差し込まれたとしても、その関数が計算し終わったときには元の範囲に収まる可能性が高いからだ。
ただ、結局phasor2を実行したときには0~1のレンジに丸まるので大きな問題にはならない。ある関数がある範囲に収まることが保証されているということは、仮にそこで使われているselfに不正な値が差し込まれたとしても、その関数が計算し終わったときには元の範囲に収まる可能性が高いからだ。また関連性のないデータが差し込まれたとしても、結局は聴感上違和感がない程度に留まる可能性もある。
さらに、実際にはコードの規模が大きくなっていくにつれて、関数呼び出しの深さが深くなっていくため、偶然関数呼び出しの構造が一致する可能性は小さくなっていくことと、実際のライブコーディングにおいては
ツリー構築の際に、FnCallはヒントとして関数のラベルを受け取るような変更が考えられる。無名関数はヒントなしで頑張る。
## 将来的な展望