finished basic writing

This commit is contained in:
2024-11-28 10:23:26 +00:00
parent fb6e3ea2f6
commit e9ab2e8cfa
8 changed files with 450 additions and 15 deletions

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# プログラミング・シンポジウム2024 予稿
## 開発メモ
.bibのdoiにアンダーバーが入ってるとエラーになるので、Zoteroからインポートしたら手動で修正

11
src/comparison.tex Normal file
View File

@@ -0,0 +1,11 @@
\begin{table*}[ht]
\centering
\begin{tabular}{c|c|c}\hline
\ & パラメトリックな信号処理グラフの生成 & 実際の信号処理 \\\hline
Faust & 項書き換え系マクロ & ブロックダイアグラム代数 \\
Kronos & 型レベルの計算 & 値の計算 \\
mimium & グローバル環境の評価 & \texttt{dsp}関数の評価 \\\hline
\end{tabular}
\\
\caption{\label{tab:comparison}{\it Faust、Kronos、mimiumにおけるパラメトリックな信号処理グラフと実際の信号処理で使われる意味論の違いを示した表。}}
\end{table*}

BIN
src/fbdelay_spos.pdf Normal file

Binary file not shown.

42
src/instructions.tex Normal file
View File

@@ -0,0 +1,42 @@
\begin{figure*}[ht]
\tt
\small
\begin{tabular}{lll}
MOVE & A B & R(A) := R(B) \\
MOVECONST & A B & R(A) := K(B) \\
GETUPVALUE & A B & R(A) := U(B) \\
\multicolumn{3}{l}{
\textit{(SETUPVALUE does not exist)}
}\\
GETSTATE* & A & R(A) := SPtr[SPos] \\
SETSTATE* & A & SPtr[SPos] := R(A) \\
SHIFTSTATE* & sAx & SPos += sAx \\
DELAY* & A B C & R(A) := update\_ringbuffer(SPtr[SPos],R(B),R(C)) \\
\multicolumn{3}{l}{
\textit{ *(SPos,SPtr)= vm.closures[vm.statepos\_stack.top()].state }
}\\
\multicolumn{3}{l}{
\textit{\quad (if vm.statepos\_stack is empty, use global state storage.)}
}\\
JMP & sAx & PC +=sAx \\
JMPIFNEG & A sBx & if (R(A)<0) then PC += sBx \\
CALL & A B C & R(A),...,R(A+C-2) := program.functions[R(A)](R(A+1),...,R(A+B-1)) \\
CALLCLS & A B C & vm.statepos\_stack.push(R(A)) \\
\ & \ & R(A),...,R(A+C-2) := vm.closures[R(A)].fnproto(R(A+1),...,R(A+B-1)) \\
\ & \ & vm.statepos\_stack.pop() \\
CLOSURE & A Bx & vm.closures.push(closure(program.functions[R(Bx)])) \\
& & R(A) := vm.closures.length - 1 \\
CLOSE & A & close stack variables up to R(A)\\
RETURN & A B & return R(A), R(A+1)...,R(A+B-2) \\
ADDF & A B C & R(A) := R(B) as float + R(C) as float\\
SUBF & A B C & R(A) := R(B) as float - R(C) as float\\
MULF & A B C & R(A) := R(B) as float * R(C) as float\\
DIVF & A B C & R(A) := R(B) as float / R(C) as float\\
ADDI & A B C & R(A) := R(B) as int + R(C) as int \\
\ &
\multicolumn{2}{l}{
\textit{...(それぞれの基本型における算術命令などが続く)}
}
\end{tabular}
\caption{\label{fig:instruction}{\it$\lambda_{mmm}$を実行するVMのための命令セットの抜粋。}}
\end{figure*}

Binary file not shown.

View File

@@ -3,11 +3,10 @@
\documentclass{ipsjprosym} \documentclass{ipsjprosym}
%\documentclass[withpage,english]{ipsjprosym} %\documentclass[withpage,english]{ipsjprosym}
\usepackage[dvips]{graphicx} \usepackage[dvipdfmx]{graphicx}
\usepackage{latexsym} \usepackage{latexsym}
\usepackage{amsmath,amssymb,amsfonts,amsthm} \usepackage{amsmath,amssymb,amsfonts,amsthm}
\usepackage{listings,listings-rust}
\begin{document} \begin{document}
@@ -59,7 +58,7 @@ Unit Generatorコンセプトに基づく言語の問題点は、既存の音楽
ラムダ計算のような、より汎用的な計算モデルに基づく信号処理の計算モデルを提案することは、さまざまな汎用言語間の相互運用を可能にするほか、既存の最適化手法の流用や、コンパイラやランタイムの実装を容易にする可能性をもつ\footnote{これまで、BDAはモナドの高位抽象化であるアローとしてラムダ計算ベースの汎用関数型言語に変換可能なことが証明されてはいる[3]。ただ、汎用関数型言語の内部DSLとして信号処理を範疇に入れた音楽プログラミング言語を実装するには、多くの言語では動的なメモリ割り当て・解放のタイミングがユーザー側で制御できないなど、ハードリアルタイム処理に適さないという問題が残る。} ラムダ計算のような、より汎用的な計算モデルに基づく信号処理の計算モデルを提案することは、さまざまな汎用言語間の相互運用を可能にするほか、既存の最適化手法の流用や、コンパイラやランタイムの実装を容易にする可能性をもつ\footnote{これまで、BDAはモナドの高位抽象化であるアローとしてラムダ計算ベースの汎用関数型言語に変換可能なことが証明されてはいる[3]。ただ、汎用関数型言語の内部DSLとして信号処理を範疇に入れた音楽プログラミング言語を実装するには、多くの言語では動的なメモリ割り当て・解放のタイミングがユーザー側で制御できないなど、ハードリアルタイム処理に適さないという問題が残る。}
Kronos[4]とW-calculus[5]は、それぞれFaustに影響を受けたラムダ計算ベースの抽象化の例である。Kronosは理論的基盤としてSystem-$F\omega$ というラムダ計算のバリエーションに基づいており、型を入力として受け取り、新しい型を返す関数を定義することができる。Kronosでは、型レベルの計算が信号処理グラフの生成に対応し、値の計算が実際の処理に対応する。Kronosにおいては遅延だけが唯一の特殊なプリミティブ演算であり、フィードバックを伴うルーティングは型計算における再帰的関数適用として表現される。ただし、意味論の厳密な形式化はなされていない。 Kronos\cite{norilo2015}とW-calculus\cite{arias2021}は、それぞれFaustに影響を受けたラムダ計算ベースの抽象化の例である。Kronosは理論的基盤としてSystem-$F\omega$ というラムダ計算のバリエーションに基づいており、型を入力として受け取り、新しい型を返す関数を定義することができる。Kronosでは、型レベルの計算が信号処理グラフの生成に対応し、値の計算が実際の処理に対応する。Kronosにおいては遅延だけが唯一の特殊なプリミティブ演算であり、フィードバックを伴うルーティングは型計算における再帰的関数適用として表現される。ただし、意味論の厳密な形式化はなされていない。
W-calculusは、変数の過去の値にアクセスする機能すなわち遅延とともに、プリミティブ操作としてフィードバックを含む。W-calculusでは、定理証明支援システムCoqを用いた厳密な意味論の定義がなされている。他方で、W-calculusは記述する対象をフィルタやリバーブのような線形時不変システムに限定している。その代わりに、書かれたシステムではシステムの線形性が保証されるほか、異なる表記のシステム間の同一性を証明することなどが可能になる。またもう1つの制限として、W-calculusでは高階関数の使用は許されていない。 W-calculusは、変数の過去の値にアクセスする機能すなわち遅延とともに、プリミティブ操作としてフィードバックを含む。W-calculusでは、定理証明支援システムCoqを用いた厳密な意味論の定義がなされている。他方で、W-calculusは記述する対象をフィルタやリバーブのような線形時不変システムに限定している。その代わりに、書かれたシステムではシステムの線形性が保証されるほか、異なる表記のシステム間の同一性を証明することなどが可能になる。またもう1つの制限として、W-calculusでは高階関数の使用は許されていない。
@@ -72,23 +71,384 @@ W-calculusは、変数の過去の値にアクセスする機能すなわち
\input{syntax.tex} \input{syntax.tex}
% 行けたら評価環境の定義をきちんとする
\ref{fig:syntax_v}$\lambda_{mmm}$の型と項の定義を示す。実際のmimiumの型システムではタプルのような合成型も使用されているが、本稿では、議論を単純化するため合成型は割愛し、適宜必要な部分で合成型を加えるときの実装方針について注釈を加えることにする。
標準的な値呼び単純型付きラムダ計算に加えて、$e_1$$e_2$サンプル分過去の値を参照する $delay\ n\ e_1\ e_2$ と、$e$ そのものの評価中に1単位時刻前の $e$ の評価結果を $x$ として参照できる抽象化 $feed x\ x.e$ の2つの項が導入されている。$delay$では使用するメモリを有限に制限するため、最大遅延として$n$をリテラルな値として明示する必要がある。
\subsection{mimiumにおけるfeed項の糖衣構文}
\label{sec:mimium}
mimiumには、関数定義の中でその関数の直前の時刻の返り値を参照するためのキーワード\texttt{self}がある。入力ゲインとフィードバックゲインの和が1になるように入力信号と最後の出力信号を混合する、単極フィルタ積分器を表す関数の例を図\ref{fig:onepole-code}に示す。このコードは $\lambda_{mmm}$ では図\ref{fig:onepole}のように表現できる。
\begin{figure}[ht]
\centering
\begin{verbatim}
fn onepole(x,g){
x*(1.0-g) + self*g
}
\end{verbatim}
\caption{\label{fig:onepole-code}{mimiumにおける単極フィルタの実装のサンプル。}}
\end{figure}
\begin{figure}[ht]
\begin{equation*}
\centering
\begin{aligned}
let\ & onepole = \\
& \ \lambda x. \lambda g.\ feed\ y.\ x *(1.0 - g) + y * g \ in\ ...
\end{aligned}
\end{equation*}
\caption{\label{fig:onepole}{$\lambda_{mmm}$における図\ref{fig:onepole-code}と等価な表現。}}
\end{figure}
\subsection{型付け規則}
\label{sec:typing}
\input{typing.tex} \input{typing.tex}
典型的な単純型付きラムダ計算に追加される $\lambda_{mmm}$ の型付け規則を図\ref{fig:typing}に示す。プリミティブ型には、ほとんどの信号処理で使われる実数型と、ディレイの添字などに使われる自然数型がある。$\lambda_{mmm}$ の設計に直接影響を与えたW-calculusでは、関数型は実数のタプルを取り、実数のタプルを返すことしかできない。これはすなわち高階関数の定義を制限していることを意味する。高階関数は実行時にクロージャのような動的メモリ割り当てに依存するデータ構造を必要とするので、この制約は信号処理言語として合理的でな一方で、ラムダ計算の汎用性を失わせてしまってもいる。
$\lambda_{mmm}$ では、クロージャのメモリ割り当ての問題はランタイム側の設計と実装に先送りすることで(\ref{sec:vm}を参照)、高階関数の使用を許している。しかし、$feed$の抽象化では、関数型を含まない型\footnote{タプルを含む合成型を扱う場合、単純な関数型のみならずメンバに関数型を1つも含まない型を意味する。}のみを許す。$feed$抽象で関数型を許可するということは、例えば1サンプル前の関数から現在の時刻に使用する関数を合成する、といった記述を可能にすることになる。しかしこれを可能にすると、クロージャによる動的メモリ割り当ての問題に加えて、時間経過ごとに新しいクロージャが過去のクロージャを参照し続けることで使用するメモリサイズが肥大化する空間漏洩space leakという更なる問題が発生する。このため、現在mimiumでは専ら実装を簡易化するという理由で関数型を含む型を$feed$の項では取れないようにしている。
\subsection{ナイーブな操作的意味論}
\label{sec:semantics}
\input{semantics.tex} \input{semantics.tex}
\section{論文1ページ目の情報} \ref{fig:semantics}$\lambda_{mmm}$ のビッグステップ(評価の中間過程を考えず、最終的な評価結果のみを考える)操作的意味論の抜粋を示す。この意味論では各時刻にごとに評価環境が用意され、現在時刻が $n$ における、 $t$ サンプル過去の評価環境を $E^{n-t}$ と呼ぶ。0より小さい時刻の評価環境が参照された場合は、どの項もその型のデフォルト値数値型の場合は0として評価される。
当然ながら、この意味論を直接実行しようとすると、各サンプルごとに時刻0から現在時刻までを再計算し、各ステップですべての変数環境を保存する必要がある。しかし実際には、 $delay$$feed$ が使用する内部メモリ空間を考慮した仮想マシンが定義され、項は実行前にこのマシン用の命令にコンパイルされる。
以下,通常の論文と同様の形式で記述して下さい.
\section{まとめ}
本テンプレートでは,プログラミング・シンポジウム向けの原稿を, \section{VMおよび命令セットの設計}
\LaTeX を用いて準備する方法についてごく簡単に示した. \label{sec:vm}
$\lambda_{mmm}$ を実行するための仮想マシン(VM)モデルとその命令セットは、 Luaバージョン5のVM\cite{ierusalimschy2005}に基づく。
ラムダ計算をベースとした計算モデルを実行する際の重要な課題は、クロージャと呼ばれるデータ構造を扱うことである。クロージャは、入れ子になった関数を定義した場所での変数環境をキャプチャし、外部関数のコンテキストにある変数を参照できるようにする。例えばクロージャを内側の関数の定義とそこで使われる変数と値の辞書との対として定義すれば、コンパイラ(もしくはインタプリタ)の実装は簡単だが、実行時の性能は制限される。
逆に、クロージャ変換(ラムダ・リフティング)と呼ばれる処理を使えば、実行時のパフォーマンスを向上させることができる。この処理では、内部関数が参照するすべての外部変数を解析し、内部関数の隠し引数として追加するような変換を行う。しかし、コンパイラーでのこの変換の実装は比較的複雑になる。
LuaのVMは、2つの方法の中間的なアプローチである上位値\textit{upvalue})という概念を採用しており、命令 \texttt{GETUPVALUE}\texttt{SETUPVALUE} を追加することで、実行時に外部変数を動的に参照できるようになっている。上位値を使ったコンパイラとVMの実装は、クロージャの完全な変換よりも単純でありながら、大幅な性能低下を避けることができる。このアプローチでは、クロージャが元の関数のコンテキストをエスケープしない限り、外部変数はヒープメモリではなくコールスタックを介してアクセスされる\cite{nystrom2021}。さらに、upvalueは他のプログラミング言語との相互運用性を促進する。例えばLua用の外部ライブラリをC言語で実装する場合、プログラマはC API経由でランタイムのコールスタック上の値だけでなく、Luaランタイムのupvalueにアクセスすることもできる。
\subsection{命令セット}
\label{sec:instruction}
LuaのVM命令と $\lambda_{mmm}$ のVM命令は以下の点で異なる。
\begin{enumerate}
\item mimiumはLuaと異なり静的型付け言語であるため、基本的な算術演算の命令が型ごとに用意されている。
\item 静的型付けと状態付き高階関数を管理するために、呼び出し操作が通常の関数呼び出しとクロージャ呼び出しに分割されている(詳細は\ref{sec:vmstructure}を参照)。
\item 条件文は\texttt{JMP}\texttt{JMPIFNEG} の2つの命令を組み合わせて実装されているが、Lua VM では専用の \texttt{TEST} 命令が採用されている。
\item forループに関連する命令、オブジェクト指向プログラミングで使用される\texttt{SELF}命令、変数へのメタデータ参照に関連する\texttt{TABLE}命令は不要なため、mimiumでは省略している。
\item リスト、タプル、配列のようなデータ構造の実装は、本稿で示す $\lambda_{mmm}$ の記述の範囲外であるため除外している\footnote{実際のコンパイラでは、タプルのようなデータ型は、型に応じてアドレスのオフセットを事前に計算し、単純な型に対する命令として分解されているため、ランタイム上でタプル用の特別な命令は持っていない。}
\end{enumerate}
$\lambda_{mmm}$ のVMは、Lua VMバージョン5以降と同様にレジスタマシンとして動作する。ただし、通常のレジスタマシンとは異なり、物理レジスタは使うわけではない。命令内でのレジスタ番号は単に、VM実行中のベースポインタに対するコールスタック上のオフセットを指す。またほとんどの命令の最初のオペランドは、演算結果が格納されるレジスタ番号を指定する。
\input{instructions.tex}
命令の一覧を図\ref{fig:instruction}に示す基本的な算術演算は一部省略している。命令の表記は、Lua VMのドキュメント\cite[p.13]{ierusalimschy2005}に概説されている形式に従っている。左から順に、演算名、オペランドのリスト、演算の擬似コードが表示される。3つのオペランドがそれぞれ符号なし8ビット整数として使用される場合、\texttt{A B C}と表現される。オペランドが符号付き整数として使用される場合は、その前に \texttt{s} が付く。2つのオペランドフィールドを組み合わせて16ビットの値にする場合、接尾辞 \texttt{x} が付加される。例えば、\texttt{B}\texttt{C} を結合して符号付き 16 ビット値として扱う場合、\texttt{sBx} と表現される。
擬似コードでは、\texttt{R(A)} は現在の関数のベースポインタ + \texttt{A} にあるレジスタ(またはコールスタック)に出し入れされるデータを示す。\texttt{K(A)}はコンパイル済みプログラムの静的変数セクションの \texttt{A} 番目のエントリを指し、\texttt{U(A)}は現在の関数の \texttt{A} 番目のupvalueにアクセスする。
Luaのupvalue演算に加えて、$delay$$feed$ 式のコンパイルを処理するために、 \texttt{GETSTATE}\texttt{SETSTATE}\texttt{SHIFTSTATE}\texttt{DELAY}の4つの新しい演算が導入されている。
\subsection{VMの構造}
\label{sec:vmstructure}
\begin{figure*}[t]
\centerline{\includegraphics[width=\hsize]{lambdammm_vm_structure.pdf}}
\caption{\label{fig:vmstructure}{$\lambda_{mmm}$を実行するための仮想機械、プログラム、またインスタンス化されたクロージャの概要を表した図。}}
\end{figure*}
\ref{fig:vmstructure}に、 $\lambda_{mmm}$ のVM、プログラムと、 実行中にインスタンス化されたクロージャの関係を示す。通常のコールスタックに加えて、VMは、フィードバックと遅延のための内部状態データを管理する専用の記憶領域単純な配列を持つ。
この記憶領域には、\texttt{GETSTATE}命令と\texttt{SETSTATE}命令で内部ステートデータを取得する位置を示すポインタが付随している。これらの位置は、\texttt{SHIFTSTATE}命令を使用して前方または後方に移動される。状態記憶メモリ内の実際のデータレイアウトは、コンパイル時に \texttt{self}\texttt{delay}、およびその他の状態関数を含む関数呼び出しを解析して静的に決定される。\texttt{DELAY}操作は2つの入力として\texttt{B}は入力される信号、\texttt{C}は遅延時間を受け取る。
しかし、高階関数--別の関数を引数として受け取ったり、1つの関数を返したりする関数--では、渡された関数の内部状態のメモリレイアウトはコンパイル時には不明である。そのため、インスタンス化された各クロージャには、VMインスタンスが保持するグローバルな内部状態記憶領域とは別の内部状態記憶領域が割り当てられる。また、VMはインスタンス化されたクロージャの内部状態記憶域のポインタを追跡するために、コールスタックとは別に状態ストレージポインタを記録するスタック以下、\textbf{状態スタック}と呼ぶ)を使用する。\texttt{CALLCLS}操作が実行されるたびに、VMはクロージャの内部状態記憶のポインタを状態スタックにプッシュする。クロージャの呼び出しが完了すると、VMは内部状態記憶ポインタを状態スタックからポップする。
インスタンス化されたクロージャは、それ自身の上位値(upvalue)用の記憶領域も保持する。クロージャが親関数のコンテキストにいる「オープンなクロージャ」とここでは呼ぶ限り、その上位値は、現在のスタックのベースポインタから負のオフセットを保持する。このオフセットはコンパイル時に決定され、プログラム内の関数のプロトタイプに格納される。さらに、上位値はローカル変数だけでなく、親関数の上位値を参照することもあるこの状況は少なくとも3つの関数が入れ子になっている場合に発生する。つまり、値が親関数のローカルなスタック変数かさらなる上位値かを示すタグと、それに対応するインデックススタックの負のオフセットまたは親関数の上位値インデックスである。
例えば、プログラム中のupvalueインデックスが\texttt{\[upvalue(1), local(3)\]}と指定されている場合を考える。この場合、\texttt{GETUPVALUE 6 1} という命令は、(\texttt{upvalue(1)}によって参照される)上位値一覧のインデックス \texttt{3} にある値を、ベースポインタに対して相対的に \texttt{R(-3)} から取得し、その結果を \texttt{R(6)} に格納することを示す。
クロージャが \texttt{RETURN} 命令によって元の関数コンテキストから抜け出すとき、事前に挿入された \texttt{CLOSE} 命令によってアクティブな上位値はスタックからヒープメモリに退避させられる。これらの上位値は、特にネストしたクロージャを含む場合には、複数の場所から参照される可能性がある。したがって、これらの上位値が使用されなくなった時にはガベージコレクションでメモリを解放する必要がある\footnote{現在のmimiumの実装では参照カウント方式でメモリを開放している。}
$\lambda_{mmm}$ は値呼びラムダ計算であり、再代入が存在しないので、 \texttt{SETUPVALUE}命令は省略される。再代入が許可されているような意味論を追加する場合、オープンなものも含めupvalueは共有メモリセルとして実装される必要がある。
\subsection{VM命令へのコンパイル例}
\begin{figure}[ht]
\centering
\begin{verbatim}
CONSTANTS:[1.0]
fn onepole(x,g) state_size:1
MOVECONST 2 0 // load 1.0
MOVE 3 1 // load g
SUBF 2 2 3 // 1.0 - g
MOVE 3 0 // load x
MULF 2 2 3 // x * (1.0-g)
GETSTATE 3 // load self
MOVE 4 1 // load g
MULF 3 3 4 // self * g
ADDF 2 2 3 // compute result
GETSTATE 3 // prepare return value
SETSTATE 2 // store to self
RETURN 3 1
\end{verbatim}
\caption{\label{fig:bytecode_onepole}{\ref{fig:onepole-code}の単極フィルターのコードをVM命令にコンパイルした際の例。}}
\end{figure}
\ref{fig:bytecodes_onepole}は図\ref{fig:onepole-code}で示したmimiumでの単極フィルターのコードをVMの命令列にコンパイルした結果の例である。\texttt{self}が参照されると、\texttt{GETSTATE}命令で値を取得し、\texttt{SETSTATE}命令で戻り値を格納して内部状態を更新してから、\texttt{RETURN}命令で値を返す。この場合、実際の戻り値は2番目の \texttt{GETSTATE} 命令を使用して取得され、時刻0の時にはデフォルトの値数値型なら0を返すようになる。
例えば、$\lambda_{mmm}$ でサンプル単位の時刻カウンタを $feed x. x+1$ と書いたとき、時刻 = 0 のときの戻り値を 0 にするか 1 にするかは、コンパイラの設計次第である。もしコンパイラが時刻=0で1を返すような設計にする場合、2番目の\texttt{GETSTATE}命令は省略でき、\texttt{RETURN}命令の値は\texttt{R(2)}となる(これは厳密には図\texttt{fig:semantics}のE-FEEDの意味論に反する
\begin{figure}[ht]
\centering
\begin{verbatim}
fn fbdelay(x,fb,dtime){
x + delay(1000,self,dtime)*fb
}
fn twodelay(x,dtime){
fbdelay(x,dtime,0.7)
+fbdelay(x,dtime*2,0.8)
}
fn dsp(x){
twodelay(x,400)+twodelay(x,800)
}
\end{verbatim}
\caption{\label{fig:fbdelay_code}{mimiumにおける、\texttt{self}とtexttt{delay}を組み合わせたフィードバックディレイの関数を複数回使用するコードの例。}}
\end{figure}
\begin{figure}[ht]
\centering
\begin{verbatim}
CONSTANTS:[0.7,2,0.8,400,800,0,1]
fn fbdelay(x,fb,dtime) state_size:1004
MOVE 3 0 //load x
GETSTATE 4 //load self
SHIFTSTATE 1 //shift Spos
DELAY 4 4 2 //delay(_,_,_)
MOVE 5 1 // load fb
MULF 4 4 5 //delayed val *fb
ADDF 3 3 4 // x+
SHIFTSTATE -1 //reset SPos
GETSTATE 4 //prepare result
SETSTATE 3 //store to self
RETURN 4 1 //return previous self
fn twodelay(x,dtime) state_size:2008
MOVECONST 2 5 //load "fbdelay"
MOVE 3 0
MOVE 4 1
MOVECONST 5 0 //load 0.7
CALL 2 3 1
SHIFTSTATE 1004 //1004=fbdelay
MOVECONST 3 5 //load "fbdelay"
MOVE 4 0
MOVECONST 5 1 //load 2
MULF 4 4 5
MOVECONST 5 0 //load 0.7
CALL 3 3 1
ADDF 3 3 4
SHIFTSTATE -1004
RETURN 3 1
fn dsp (x)
MOVECONST 1 6 //load "twodelay"
MOVE 2 0
MOVECONST 3 3 //load 400
CALL 1 2 1
SHIFTSTATE 2008
MOVECONST 2 6 //load "twodelay"
MOVE 2 3
MOVE 3 0
MOVECONST 3 4 //load 400
CALL 2 2 1
ADD 1 1 2
SHIFTSTATE -2008
RETURN 1 1
\end{verbatim}
\caption{\label{fig:bytecodes_fbdelay}{\ref{fig:fbdelay_code}のフィードバックディレイの例をVM命令列にコンパイルした例。}}
\end{figure}
\begin{figure}[ht]
\centerline{\includegraphics[width=0.7\hsize]{fbdelay_spos.pdf}}
\caption{\label{fig:fbdelay_spos}{\it\ref{fig:bytecodes_fbdelay}の命令列を実行している間の、内部状態ストレージ上を移動する状態位置ポインタの移動過程を概念的に示した図。}}
\end{figure}
\ref{fig:fbdelay_code} と 図\ref{fig:bytecodes_fbdelay}に、より複雑なコードの例とそのコンパイル結果のVM命令列の例を示した。このコードでは\texttt{fbdelay}としてフィードバック付きのディレイ関数を定義して、別の関数\texttt{twodelay}では異なる引数を使う2つのフィードバック付きディレイを呼び出す。さらに、 \texttt{dsp}\texttt{twodelay} 関数を2つ使う。
この例ではVM命令の中で、\texttt{GETSTATE} 命令で \texttt{self} を参照した後、または別の内部状態付き関数を呼び出した後に、\texttt{SHIFTSTATE} 命令が挿入され、次の(非クロージャ)関数呼び出しに備えて内部状態の読み書き位置を移動している。関数が終了する前には、再び\texttt{SHIFTSTATE}命令を使用して、読み書き位置を現在の関数の実行開始時の状態にリセットしている。図\ref{fig:fbdelay_spos}は、\texttt{twodelay}関数の実行中に\texttt{SHIFTSTATE}命令によって内部状態読み書きポインタがどのように遷移するかを示している。\texttt{SHIFTSTATE}操作の引数はワードサイズ64ビットの数値であり、ディレイのためのワードサイズは読み取り位置インデックス、書き込み位置インデックス、リングバッファの長さの値の3つが付加されるため、最大遅延時間+3となる。
以前のmimiumの実装では、内部状態を関数の呼び出しに応じた木構造として生成していたが、命令上で内部状態の読み書き位置を相対的なオフセットとして表現することで、内部状態のメモリレイアウトは単純な配列として表現可能になり、各関数の命令列ではどの関数から呼び出されるのかについて知る必要がなくなる。これはLuaのVMがクロージャ変換で全ての自由変数の参照を静的に解決するのではなく、upvalueとしてコールスタック上の相対的な位置として扱い、部分的にランタイムに上位値の解決を任せることでコンパイラの実装を簡略化しているのと似たようなアプローチと言える。
\begin{figure}[ht]
\centering
\begin{verbatim}
fn bandpass(x,freq){
//...
}
fn filterbank(n,filter_factory:
()->(float,float)->float){
if (n>0){
let filter = filter_factory()
let next = filterbank(n-1,filter_factory)
|x,freq| filter(x,freq+n*100)
+ next(x,freq)
}else{
|x,freq| 0
}
}
let myfilter = filterbank(3,| | bandpass)
fn dsp(){
myfilter(x,1000)
}
\end{verbatim}
\caption{\label{fig:filterbank_good}{再帰関数とクロージャを使ってパラメトリックにフィルターを複製するコードの例。}}
\end{figure}
さらに別の例として図\ref{fig:filterbank_good}に高階関数\texttt{filterbank}を使うものを示す。この関数は、別の関数\texttt{filter}(これは入力と周波数を引数として受け取る)を引数として受け取り、\texttt{filter}のインスタンスを\texttt{n}個複製し、それらを足し合わせるものである\footnote{以前のmimiumの仕様\cite{Matsuura2021}では、変数束縛と破壊的代入の構文は同じ(\texttt{x = a})だった。しかし、新しいバージョンの構文では、変数の束縛に\texttt{let}キーワードを使用している。}
以前のmimiumのコンパイラは、内部状態のツリー全体をコンパイル時に静的に決定する必要があったため、このような内部状態を引数に取る関数をコンパイルすることができなかった。しかし、$\lambda_{mmm}$ のVMではこれを動的に解決することができる。図\ref{fig:bytecode_filterbank}に、このコードをVM命令にコンパイルした例を示す。\texttt{filterbank}の1行目の再帰呼び出しや、引数として渡された関数や(\texttt{filter}のように)upvalueとして取得した関数の呼び出しは、\texttt{CALL}命令ではなく\texttt{CALLCLS}命令を使って実行される。ここで、\texttt{GETSTATE}命令や \texttt{SETSTATE} 命令はこの関数では使用されない。これは、\texttt{CALLCLS} 命令が実行される際に、使用する内部状態記憶領域が動的に切り替わるためである。
\begin{figure}[ht]
\centering
\begin{verbatim}
CONSTANTS[100,1,0,2]
fn inner_then(x,freq)
//upvalue:[local(4),local(3),
// local(2),local(1)]
GETUPVALUE 3 2 //load filter
MOVE 4 0
MOVE 5 1
GETUPVALUE 6 1 //load n
ADDD 5 5 6
MOVECONST 6 0
MULF 5 5 6
CALLCLS 3 2 1 //call filter
GETUPVALUE 4 4 //load next
MOVE 5 0
MOVE 6 1
CALLCLS 4 2 1 //call next
ADDF 3 3 4
RETURN 3 1
fn inner_else(x,freq)
MOVECONST 2 2 //load 0
RETURN 2 1
fn filterbank(n,filter_factory)
MOVE 2 0 //load n
MOVECONST 3 2 //load 0
SUBF 2 2 3
JMPIFNEG 2 12
MOVE 2 1 //load filter_factory
CALL 2 2 0 //get filter
MOVECONST 3 1 //load itself
MOVE 4 0 //load n
MOVECONST 5 1 //load 1
SUBF 4 4 5
MOVECONST 5 2 //load inner_then
CALLCLS 3 2 1 //recursive call
MOVECONST 4 2 //load inner_then
CLOSURE 4 4 //load inner_lambda
JMP 2
MOVECONST 4 3 //load inner_else
CLOSURE 4 4
CLOSE 4
RETURN 4 1
\end{verbatim}
\caption{\label{fig:bytecode_filterbank}{\ref{fig:filterbank_good}のフィルタバンクの例をVM命令列にコンパイルした例。}}
\end{figure}
\section{議論}
\label{sec:discussion}
\input{comparison.tex}
\texttt{filterbank}の例で示したように、mimiumではグローバル環境の評価においてフィルタの複製などのパラメトリックな信号処理内容を高階関数として計算し、\texttt{dsp}の中で生成された関数を利用して実際の信号処理を評価している。
\ref{tab:comparison}で示すように、Faustでは項書き換えマクロを、Kronosでは型レベルの計算を用いるように、既存の言語ではパラメトリックな信号処理内容の生成とその実行に異なる意味論を持つ体系を混在させているのに対して、mimiumではグローバル環境と実際の信号処理の実行に同じ値レベルの意味論を用いている。
この単一の意味論による体系の利点としては、初心者が言語の体系の理解を簡単にする、また他の汎用言語との実行時の相互運用性を高められる、といったことが考えられる。一方で、意味論が統一されていることによって、 $\lambda_{mmm}$ は普通のラムダ計算で期待される振る舞いから逸脱した挙動を見せる問題がある。
\subsection{Let束縛の位置の違いによる挙動の変化}
\label{sec:letbinding}
\begin{figure}[ht]
\centering
\begin{verbatim}
fn filterbank(n,filter){
if (n>0){
|x,freq| filter(x,freq+n*100)
+ filterbank(n-1,filter)(x,freq)
}else{
|x,freq| 0
}
}
fn dsp(){
filterbank(3,bandpass)(x,1000)
}
\end{verbatim}
\caption{\label{fig:filterbank_bad}{\ref{fig:filterbank_good}におけるパラメトリックなフィルター複製のコードの間違った例。}}
\end{figure}
mimiumでは時間の経過とともに変化する内部状態を持つ関数を使用することで、一般的な関数型プログラミング言語と比較して、高階関数を使用した場合に直感に反する振る舞いを引き起こす。
\ref{fig:filterbank_bad}に、図\ref{fig:filterbank_good}のフィルタバンクのコード例を少しだけ変更して、間違った処理の例を示す。図\ref{fig:filterbank_bad}と図\ref{fig:filterbank_good}の主な違いは、\texttt{filterbank}関数内の再帰呼び出しを直接書いているか、内部関数の外側で\texttt{let}式で束縛しているかである。同様に、\texttt{dsp}関数mimiumのオーディオドライバから呼び出されるでも、\texttt{filterbank}関数が\texttt{dsp}内で評価されるか、グローバル環境でで一度だけ\texttt{let}で束縛されているかという違いがある。
典型的な関数型プログラミング言語では、関数の破壊的代入を伴わない限り、図\ref{fig:filterbank_good}から図\ref{fig:filterbank_bad}への変換に見られるように、\texttt{let}で束縛された変数をその項にで置き換えた(ベータ簡約)としても、計算処理の内容は変化しない。
しかしmimiumでは、評価の段階が グローバル環境の評価(信号処理グラフの具体化)および \texttt{dsp} 関数の繰り返しの実行内部状態の暗黙の更新を伴う実際の信号処理の2つに分かれている。
例に挙げたmimiumのコードには破壊的な代入は含まれていないが、図\ref{fig:filterbank_good}では、グローバル環境の評価中に\texttt{filterbank}関数の再帰的な実行が1回だけ発生する。逆に、コード\ref{fig:filterbank_bad}では、\texttt{dsp}関数が毎サンプル実行されるたびに再帰的な関数が実行され、クロージャが生成される。クロージャの内部状態はクロージャのアロケーション時に初期化されるため、図\ref{fig:filterbank_bad}の例では、\texttt{filterbank}が評価された後クロージャの内部状態は毎時刻リセットされてしまうことになる。
この問題に対処する方法として、グローバル環境評価ステージ0と実際の信号処理ステージ1のどちらで項を使うべきかを示す区別を型システムに導入することが考えられる。これは多段階計算\cite{Taha1997}を用いて実現できる。コード\ref{fig:filterbank_multi}は、BER MetaOCamlの構文を使った\texttt{filterbank}のコード例である。\verb|.<term>.|は次のステージで使われるプログラムを生成し、\verb|~term|は前のステージで評価された項を埋め込むことを意味する\cite{kiselyov2014}\texttt{filterbank}関数はステージ0で評価され、\texttt{dsp}関数の中で\verb|~|で評価結果を埋め込んでいる。FaustやKronosとは対照的に、この多段階計算の記述は信号処理グラフの生成と信号処理の実行の両方で統一された意味論を保持している。
\begin{figure}[ht]
\centering
\begin{verbatim}
fn filterbank(n,filter:
&(float,float)->float)->
&(float,float)->float{
.< if (n>0){
|x,freq| ~filter(x,freq+n*100)
+ ~filterbank(n-1,filter)(x,freq)
}else{
|x,freq| 0
} >.
}
fn dsp(){
~filterbank(3,.<bandpass>.)(x,1000)
}
\end{verbatim}
\caption{\label{fig:filterbank_multi}{mimiumの将来的な仕様における、多段階計算を使用した\texttt{filterbank}関数の例。}}
\end{figure}
\subsection{外部言語で定義される状態付き関数呼び出しの可能性}
\label{sec:ffi}
クロージャのデータは、図\ref{fig:vmstructure}で示したように、関数と内部状態の組み合わせで表現されている。\verb|filterbank|の例で内部状態に対して特別な操作を必要としていないということは、mimiumからCやC++で書かれた発振器やフィルターなどのUGenを、通常のクロージャと同じように呼び出すことができることを意味する。さらに、外部UGenをパラメトリックに複製・合成することも可能になる。この機能はFaustや類似の言語では実装が難しいが、 $\lambda_{mmm}$ の設計では簡単に実現できる。
ただし、現在mimiumはサンプル単位の処理を基本にしており、バッファ単位の信号を扱うことができない。ほとんどのネイティブに実装されたUGenはバッファ単位でデータを処理するため、今のところ、既存の外部UGenが使用可能な実用的ケースは多くない。しかしその上で、厳密にサンプル単位の処理を必要とするのは$feed$ の項のみであるため、一度に1サンプルしか処理できない関数と、一括して複数サンプルを処理する関数とを型レベルで区別することは可能なはずである。Faustでマルチレートの仕様が検討されているように、バッファ単位で処理できる部分をコンパイラが自動的に判断することで、外部ユニットジェネレータ間とバッファ単位での連携も可能かもしれない。
\section{結論}
\label{sec:conclusion}
本稿では、音楽・信号処理用プログラミング言語の中間表現 $\lambda_{mmm}$ と、それを実行するための仮想マシン・命令セットを提案した。 $\lambda_{mmm}$ は、信号処理グラフの生成と、その実際の処理を統一された構文と意味論で記述可能にする。ただし、関数がグローバル環境で評価されるかDSPの繰り返し実行で評価されるかの判別はユーザーの責任となり、初学者にはその区別が難しいという欠点がある。
また本論文では、VMの動作を記述する擬似コードとに加えて、mimiumでのコードの例とそれに対応するバイトコードの例を示すことでコンパイル過程を説明したのみである。より正式な意味論と詳細なコンパイル過程の提示は、今後多段階計算の導入も踏まえ検討する必要がある。
この研究が、デジタル/コンピュータ上での音・音楽のより一般的な表現に貢献し、音楽のための言語理論とプログラミング言語理論のより広範な分野との間のより深いつながりを促進することを期待する。
本テンプレートに関する質問・バグ報告は,
第56回プログラミングシンポジウム予稿集担当松崎公紀\verb|matsuzaki.kiminori@kochi-tech.ac.jp|
まで連絡下さい.
\begin{acknowledgment} \begin{acknowledgment}
mimiumの開発は、2019年度未踏IT人材発掘・育成事業の支援の元開発された。また本研究は、日本学術振興会科研費若手研究「音楽と工学の相互批評的実践としての「音楽土木工学」の研究 」23K12059の助成を受けている.ここに感謝の意を表する。 mimiumの開発は、2019年度未踏IT人材発掘・育成事業の支援の元開発された。また本研究は、日本学術振興会科研費若手研究「音楽と工学の相互批評的実践としての「音楽土木工学」の研究 」23K12059の助成を受けている.ここに感謝の意を表する。

View File

@@ -93,7 +93,7 @@
} }
@inproceedings{kiselyov2014, @inproceedings{kiselyov2014,
title = {The {{Design}} and {{Implementation}} of {{BER~MetaOCaml}}}, title = {The {{Design}} and {{Implementation}} of {{BER MetaOCaml}}},
booktitle = {Functional and {{Logic Programming}}}, booktitle = {Functional and {{Logic Programming}}},
author = {Kiselyov, Oleg}, author = {Kiselyov, Oleg},
editor = {Codish, Michael and Sumii, Eijiro}, editor = {Codish, Michael and Sumii, Eijiro},
@@ -101,8 +101,7 @@
pages = {86--102}, pages = {86--102},
publisher = {Springer International Publishing}, publisher = {Springer International Publishing},
address = {Cham}, address = {Cham},
doi = {10.1007/978-3-319-07151-0_6}, doi = {10.1007/978-3-319-07151-0\_6},
abstract = {MetaOCaml is a superset of OCaml extending it with the data type for program code and operations for constructing and executing such typed code values. It has been used for compiling domain-specific languages and automating tedious and error-prone specializations of high-performance computational kernels. By statically ensuring that the generated code compiles and letting us quickly run it, MetaOCaml makes writing generators less daunting and more productive.},
isbn = {978-3-319-07151-0}, isbn = {978-3-319-07151-0},
language = {en} language = {en}
} }
@@ -220,7 +219,7 @@
volume = {39}, volume = {39},
number = {4}, number = {4},
pages = {30--48}, pages = {30--48},
doi = {10.1162/COMJ_a_00330}, doi = {10.1162/COMJ\_a\_00330},
abstract = {Kronos is a signal-processing programming language based on the principles of semifunctional reactive systems. It is aimed at efficient signal processing at the elementary level, and built to scale towards higher-level tasks by utilizing the powerful programming paradigms of "metaprogramming" and reactive multirate systems. The Kronos language features expressive source code as well as a streamlined, efficient runtime. The programming model presented is adaptable for both sample-stream and event processing, offering a cleanly functional programming paradigm for a wide range of musical signal-processing problems, exemplified herein by a selection and discussion of code examples.}, abstract = {Kronos is a signal-processing programming language based on the principles of semifunctional reactive systems. It is aimed at efficient signal processing at the elementary level, and built to scale towards higher-level tasks by utilizing the powerful programming paradigms of "metaprogramming" and reactive multirate systems. The Kronos language features expressive source code as well as a streamlined, efficient runtime. The programming model presented is adaptable for both sample-stream and event processing, offering a cleanly functional programming paradigm for a wide range of musical signal-processing problems, exemplified herein by a selection and discussion of code examples.},
file = {/Users/tomoya/Zotero/storage/THAKVEM6/m-api-574ff3be-cfe2-7867-406a-df50770bf1cb.pdf} file = {/Users/tomoya/Zotero/storage/THAKVEM6/m-api-574ff3be-cfe2-7867-406a-df50770bf1cb.pdf}
} }
@@ -295,3 +294,20 @@
urldate = {2024-11-27}, urldate = {2024-11-27},
howpublished = {http://modlfo.github.io/vult/} howpublished = {http://modlfo.github.io/vult/}
} }
@article{Taha1997,
title = {Multi-{{Stage Programming}} with {{Explicit Annotations}}},
author = {Taha, Walid and Sheard, Tim},
year = {1997},
month = dec,
journal = {SIGPLAN Notices (ACM Special Interest Group on Programming Languages)},
volume = {32},
number = {12},
pages = {203--214},
publisher = {Association for Computing Machinery (ACM)},
issn = {03621340},
doi = {10.1145/258994.259019},
urldate = {2021-05-12},
abstract = {We introduce MetaML, a statically-typed multi-stage programming language extending Nielson and Nielson's two stage notation to an arbitrary number of stages. MetaML extends previous work by introducing four distinct staging annotations which generalize those published previously [25, 12, 7, 6] We give a static semantics in which type checking is done once and for all before the first stage, and a dynamic semantics which introduces a new concept of cross-stage persistence, which requires that variables available in any stage are also available in all future stages. We illustrate that staging is a manual form of binding time analysis. We explain why, even in the presence of automatic binding time analysis, explicit annotations are useful, especially for programs with more than two stages. A thesis of this paper is that multi-stage languages are useful as programming languages in their own right, and should support features that make it possible for programmers to write staged computations without significantly changing their normal programming style. To illustrate this we provide a simple three stage example, and an extended two-stage example elaborating a number of practical issues.},
file = {/Users/tomoya/Zotero/storage/KFYY25CM/Taha, Sheard - 1997 - Multi-Stage Programming with Explicit Annotations.pdf;/Users/tomoya/Zotero/storage/X3DDM6HN/full-text.pdf}
}

View File

@@ -39,6 +39,7 @@
&e&::=& \; x \;\; (x \in {v_p}) & [value] &\\ &e&::=& \; x \;\; (x \in {v_p}) & [value] &\\
& & |& \; \lambda x.e & [lambda] &\\ & & |& \; \lambda x.e & [lambda] &\\
& & |& \; e_1 \; e_2 & [app] &\\ & & |& \; e_1 \; e_2 & [app] &\\
& & |& \; let\; x = e_1\; in\; e_2 & [let] &\\
& & |& \; if\; (e_c)\; e_t\; else\; e_e & [if] &\\ & & |& \; if\; (e_c)\; e_t\; else\; e_e & [if] &\\
& & |& \; delay\; n \; e_1 \; e_2 \;\; (n \in \mathbb{N})& [delay] &\\ & & |& \; delay\; n \; e_1 \; e_2 \;\; (n \in \mathbb{N})& [delay] &\\
& & |& \; feed \; x.e & [feed] &\\ & & |& \; feed \; x.e & [feed] &\\