update update

This commit is contained in:
2024-11-29 04:58:01 +00:00
parent c8da09dd2e
commit 4ba4888ff7
4 changed files with 49 additions and 42 deletions

View File

@@ -10,7 +10,7 @@ GETUPVALUE & A B & R(A) := U(B) \\
}\\ }\\
GETSTATE* & A & R(A) := SPtr[SPos] \\ GETSTATE* & A & R(A) := SPtr[SPos] \\
SETSTATE* & A & SPtr[SPos] := R(A) \\ SETSTATE* & A & SPtr[SPos] := R(A) \\
SHIFTSTATE* & sAx & SPos += sAx \\ SHIFTSTATE* & sAxx & SPos += sAxx \\
DELAY* & A B C & R(A) := update\_ringbuffer(SPtr[SPos],R(B),R(C)) \\ DELAY* & A B C & R(A) := update\_ringbuffer(SPtr[SPos],R(B),R(C)) \\
\multicolumn{3}{l}{ \multicolumn{3}{l}{
\textit{ *(SPos,SPtr)= vm.closures[vm.statepos\_stack.top()].state } \textit{ *(SPos,SPtr)= vm.closures[vm.statepos\_stack.top()].state }
@@ -18,7 +18,7 @@ DELAY* & A B C & R(A) := update\_ringbuffer(SPtr[SPos],R(B),R(C)) \\
\multicolumn{3}{l}{ \multicolumn{3}{l}{
\textit{\quad (if vm.statepos\_stack is empty, use global state storage.)} \textit{\quad (if vm.statepos\_stack is empty, use global state storage.)}
}\\ }\\
JMP & sAx & PC +=sAx \\ JMP & sAxx & PC += sAxx \\
JMPIFNEG & A sBx & if (R(A)<0) then PC += sBx \\ 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)) \\ 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)) \\ CALLCLS & A B C & vm.statepos\_stack.push(R(A)) \\

View File

@@ -20,7 +20,7 @@
\begin{abstract} \begin{abstract}
本発表では筆者の開発する音楽のためのプログラミング言語mimiumの理論的基盤について、音楽向け言語の歴史的文脈に沿って解説する。mimiumは、リアルタイム信号処理を想定した音楽用のDSLだが、既存の多くの言語異なり、Unit Generatorのような特定の音楽表現に基づくプリミティブを用意しない。代わりに、値呼び単純型付きラムダ計算に遅延とフィードバックという2要素をプリミティブとして追加した中間表現を定義することで、その言語上でほとんどの信号処理アルゴリズムを関数のパイプとして表現できる。また、Luaを参考にしたVMを定義することで、内部状態を持つ信号処理関数の高階関数を用いた複製や、ホスト環境の埋めこみを容易に可能している。音楽というドメインに特化しながらも、汎用性を失わない言語の意義について議論する。 本発表では筆者の開発する音楽のためのプログラミング言語mimiumの理論的基盤について、音楽向け言語の歴史的文脈に沿って解説する。mimiumは、リアルタイム信号処理を想定した音楽用のDSLだが、既存の多くの言語とは異なり、Unit Generatorのような特定の音楽表現に基づくプリミティブを用意しない。代わりに、値呼び単純型付きラムダ計算に遅延とフィードバックという2要素をプリミティブとして追加した中間表現を定義することで、その言語上でほとんどの信号処理アルゴリズムを関数のパイプとして表現できる。また、Luaを参考にしたVMを設計したことで、内部状態を持つ信号処理関数の高階関数を用いた複製や、ホスト環境の埋めこみを容易に可能している。音楽というドメインに特化しながらも、汎用性を失わない言語の意義について議論する。
\end{abstract} \end{abstract}
\begin{jkeyword} \begin{jkeyword}
@@ -32,37 +32,37 @@
% Body %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Body %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{はじめに-音楽プログラミング言語の分業化} \section{はじめに-音楽プログラミング言語の分業化}
音楽のためのプログラミング言語・環境は、Cycling'74 MaxやPure Data、CSound、SuperCollider、ChucKといった言語を代表として様々なものが開発されてきている\cite{roads2001,Lazzarini2013,Nishino2016,Dannenberg2018}。こうした言語の多くは歴史を遡ると1950年代にベル研究所でマックス・マシューズらが開発したMUSICシリーズに遡ることができる\cite{mathews1963,park2009}。MUSICシリーズは音圧波形を時間・音圧の2次元で離散化・量子化した数列として表すことで、計算によって任意の波形を生成することが可能になるパルス符号変調という理論に基づいて計算機で音を出した最初の事例である。 音楽のためのプログラミング言語・環境は、Cycling'74 MaxやPure Data、CSound、SuperCollider、ChucKといった言語を代表として様々なものが開発されてきている\cite{roads2001,Lazzarini2013,Nishino2016,Dannenberg2018}。こうした言語の多くは歴史を遡ると1950年代にベル研究所でマックス・マシューズらが開発したMUSICシリーズに遡ることができる\cite{mathews1963,park2009}。MUSICシリーズはパルス符号変調という今日のコンピューター上のでの音声処理のすべての基礎でもある理論、すなわち音圧波形を時間・音圧の2次元で離散化・量子化した数列として表すことで、計算によって任意の波形を生成するという考えに基づいて計算機で音を出した最初の事例である。
CやC++といった汎用プログラミング言語で音声合成を行う場合も、基本的にPCMの理論に基づいてプログラミングを行うことになるが、こうした汎用の言語はメモリ管理や並行処理といったハードウェアに近い処理への理解を要求されるため、一般に音楽の記述を行う場合にはより抽象化されたライブラリや、専用のドメイン固有言語を利用するのが普通である。 CやC++といった汎用プログラミング言語で音声合成を行う場合も、基本的にPCMの理論に基づいてプログラミングを行うことになるが、こうした汎用の言語はメモリ管理や並行処理といったハードウェアに近い処理への理解を要求されるため、一般に音楽の記述を行う場合にはより抽象化されたライブラリや、専用のドメイン固有言語を利用するのが普通である。
% todo MUSICがUGENパラダイムを導入したという接続を入れる % todo MUSICがUGENパラダイムを導入したという接続を入れる
そうしたライブラリや言語では、Unit Generator(UGen)と呼ばれる、オシレーターやフィルターといった基礎的な処理単位を、モジュラーシンセサイザーの様に組み合わせていくことで処理を行う。\footnote{モジュラーシンセサイザーとUnit Generatorは実際には同時期に現れたコンセプトではあるが、音楽プログラミング言語は積極的にビジュアル的なメタファーを物理的なシンセサイザーから取り入れてきている。} そうしたライブラリや言語では、MUSIC IIIで導入されたUnit Generator(UGen)と呼ばれる、オシレーターやフィルターといった基礎的な処理単位を、モジュラーシンセサイザーの様に組み合わせていくことで処理を行う考え方に基づく\footnote{モジュラーシンセサイザーとUnit Generatorは実際には同時期に現れたコンセプトではある(\cite[p20]{park2009})が、その後の音楽プログラミング言語は積極的にビジュアル的なメタファーを物理的なシンセサイザーから取り入れてきている。}
2000年代以降では、Sonic Pi\cite{Aaron2013}やTidalCycles\cite{McLean2014}に代表される、メロディやリズムパターンをソースコードから生成し、リアルタイムでソースコードを書き換えることで演奏を行うライブコーディング環境が開発されてきたが、これらの言語は信号処理をSuperColliderなどのUGenベースの言語に任せ、それらの言語ランタイムへ命令を送信をするクライアントとして実装されている。そして、多くの言語でUGen自体の実装にはC++などの低級言語が用いられ、動的ロードライブラリの形で提供される。 2000年代以降では、Sonic Pi\cite{Aaron2013}やTidalCycles\cite{McLean2014}に代表される、メロディやリズムパターンをソースコードから生成し、リアルタイムでソースコードを書き換えることで演奏を行うライブコーディングのための言語や環境が開発されてきた。こうした言語の多くは信号処理をSuperColliderなどのUGenベースの言語に任せ、それらの言語ランタイムへ命令を送信をするクライアントとして実装されている。そして、多くの言語でUGen自体の実装にはC++などの低級言語が用いられ、動的ロードライブラリの形で提供される。
%--[p42] Nishino2016にしたい %--[p42] Nishino2016にしたい
このように音楽のためのプログラミング言語は、表現可能な範囲をユーザーや実装者の関心のある領域ごとに細分化する形で発展してきている。その一方で、NishinoとNakatsuによるサーベイで指摘されている\cite{Nishino2016}ように、近年のコンピューター音楽実践ではマイクロサウンド\footnote{音声波形を細かく切り刻んで再構成するような音声合成手法\cite{roads2004}。シンセサイザーのカテゴリとして、グラニュラー合成と呼ばれる物の基礎的な考え方でもある。}や、サンプル単位精度でのイベント処理のような高度な処理が求められる中で、Unit Generatorというコンセプトを基礎に置くこと自体の妥当性が問われてもいる。 このように音楽のためのプログラミング言語は、表現可能な範囲をユーザーや実装者の関心のある領域ごとに細分化する形で発展してきている。その一方で、NishinoとNakatsuによるサーベイで指摘されている(\cite[p42]{Nishino2016})ように、近年のコンピューター音楽実践ではマイクロサウンド\footnote{音声波形を細かく切り刻んで再構成するような音声合成手法\cite{roads2004}。シンセサイザーのカテゴリとして、グラニュラー合成と呼ばれる物の基礎的な考え方でもある。}や、サンプル単位精度でのイベント処理のような高度な処理が求められる中で、Unit Generatorというコンセプトを基礎に置くこと自体の妥当性が問われてもいる。
ユーザーおよび開発者の利便性に限った点に絞ったとしても、UGenパラダイムにはいくつかの問題点が指摘できる。まず1つはコードの再利用性である。多くの言語では、実際にはほとんど同じような内容であるにも関わらず、それぞれが基本的なUGenを実装し直しており、言語間での相互運用性に欠けている。2つ目は、UGenがプリミティブなデータ型として提供されており、非線系オシレーターの用に既存のUGenの組み合わせでは表現できない新たなUGenの定義がその言語上では難しいことである。3つ目に、UGen自体のパラメトリックな接続の抽象化例えば、オシレーターを任意の本数複製したり、フィルターをカスケードしたりすることが難しい点である。これは言い換えると、多くの言語では、UGenを引数として受け取って新たなUGenを定義するような関数を定義できないということである。 ユーザーおよび開発者の利便性に限った点に絞ったとしても、UGenパラダイムにはいくつかの問題点が指摘できる。まず1つはコードの再利用性である。多くの言語では、実際にはほとんど同じような内容であるにも関わらず、それぞれが基本的なUGenを実装し直しており、言語間での相互運用性に欠けている。2つ目は、UGenがプリミティブなデータ型として提供されており、非線系オシレーターの用に既存のUGenの組み合わせでは表現できない新たなUGenの定義がその言語上では難しいことである。3つ目に、UGen自体のパラメトリックな接続の抽象化例えば、オシレーターを任意の本数複製したり、フィルターをカスケードしたりすることが難しい点である。これは言い換えると、多くの言語では、UGenを引数として受け取って新たなUGenを定義するような関数を定義できないということである。
\subsection{関数型プログラミング言語に影響を受けた信号処理言語} \subsection{関数型プログラミング言語に影響を受けた信号処理言語}
Unit Generatorコンセプトに基づく言語の問題点は、既存の音楽プログラミング言語の多くが実装に依存しており、言語としての意味論が形式化されているものがほとんど無い、とも言い換えられる。 Unit Generatorコンセプトに基づく言語の問題点は、既存の音楽プログラミング言語の多くが実装に依存しており、言語としての意味論が形式化されているものがほとんど無いことにも起因する。
この観点で、実用的に使われている音楽プログラミング言語の中では、Faust\cite{Orlarey2004}例外的に強く形式化された言語として挙げられる。Faustは、入出力を持つブロックを、並列、直列、分岐、合流、再帰の5つの合成子を用いて組み合わせていくブロックダイアグラム代数Block Diagram Algebra-BDAという体系を基礎に置く。バイパス、カット、基本的な算術演算、条件分岐、遅延をプリミティブなブロックとして提供することで、複雑な信号処理グラフを記述することができる。また後の拡張では、項書き換え系に基づくマクロを導入することで、任意の数の入出力を持つブロックの抽象化を可能にした\cite{graf2010} この観点で、Faust\cite{Orlarey2004}は実用的に使われている音楽プログラミング言語の中でも例外的に強く形式化された言語である。Faustは、入出力を持つブロックを、並列、直列、分岐、合流、再帰の5つの合成子を用いて組み合わせていくブロックダイアグラム代数Block Diagram Algebra-BDAという体系を基礎に置く。バイパス、カット、基本的な算術演算、条件分岐、遅延をプリミティブなブロックとして提供することで、複雑な信号処理グラフを記述することができる。また後の拡張では、項書き換え系に基づくマクロを導入することで、任意の数の入出力を持つブロックの抽象化を可能にした\cite{graf2010}
この形式化を通じた強力な抽象化能力により、FaustはC、C++、Rust、LLVM IRなどの様々なバックエンドにコンパイルでき、あらかじめ用意されたアーキテクチャファイルと呼ばれるボイラープレートコードと組み合わせることで、MaxやSuperColliderのUGenとしてエクスポートすることも可能になっている。その一方、Faustの理論的基盤であるBDAは、一般的なプログラミング言語との理論的・実用的な互換性に欠ける。Faustでは外部のC関数を呼び出すことは可能だが、ポインタや参照を扱えないのでホストの内部状態をFaust側から制御することは難しい。 この形式化を通じた強力な抽象化能力により、FaustはC、C++、Rust、LLVM IRなどの様々なバックエンドにコンパイルでき、あらかじめ用意されたアーキテクチャファイルと呼ばれるボイラープレートコードと組み合わせることで、MaxやSuperColliderのUGenとしてエクスポートしたり、WebAssemblyを利用してWebブラウザ上で実行することができる。その一方、Faustの理論的基盤であるBDAは、一般的なプログラミング言語との理論的・実用的な互換性に欠ける。Faustでは外部のC関数を呼び出すことは可能だが、ポインタや参照を扱えないのでホストの内部状態をFaust側から制御することは難しい。
また、Faustのマクロは、パターンマッチに基づいてBDAを生成する独立した項書き換え系である。そのため、パターンマッチングのための数値引数は暗黙のうちに整数であることが要求され、BDA上では整数と実数の方の区分けは存在しないにもかかわらず、コンパイル時エラーを起こすことある。この暗黙の型付けルールは、初学者のユーザーにとっては直感的なものとは言い難い。 また、Faustのマクロは、パターンマッチに基づいてBDAを生成する独立した項書き換え系である。そのため、パターンマッチングのための数値引数は暗黙のうちに整数であることが要求され、BDA上では整数と実数の方の区分けは存在しないにもかかわらず、コンパイル時エラーを起こすことある。この暗黙の型付けルールは、初学者のユーザーにとっては直感的なものとは言い難い。
ラムダ計算のような、より汎用的な計算モデルに基づく信号処理の計算モデルを提案することは、さまざまな汎用言語との相互運用を促進しうるほか、既存の最適化手法の流用や、コンパイラやランタイムの実装を容易にする可能性をもつ\footnote{これまで、BDAはモナドの高位抽象化であるアローとしてラムダ計算ベースの汎用関数型言語に変換可能なことが証明されてはいる[3]。ただ、汎用関数型言語の内部DSLとして信号処理を範疇に入れた音楽プログラミング言語を実装するには、多くの言語では動的なメモリ割り当て・解放のタイミングがユーザー側で制御できないなど、ハードリアルタイム処理に適さないという問題が残る。} ラムダ計算のような、より汎用的な計算モデルに基づく信号処理の計算モデルを提案することは、さまざまな汎用言語との相互運用を促進しうるほか、既存の最適化手法の流用や、コンパイラやランタイムの実装を容易にする可能性をもつ\footnote{これまで、BDAはモナドの高位抽象化であるアローとしてラムダ計算ベースの汎用関数型言語に変換可能なことが証明されてはいる[3]。ただ、汎用関数型言語の内部DSLとして信号処理を範疇に入れた音楽プログラミング言語を実装するには、多くの言語では動的なメモリ割り当て・解放のタイミングがユーザー側で制御できないなど、ハードリアルタイム処理に適さないという問題が残る。}
Kronos\cite{norilo2015}とW-calculus\cite{arias2021}は、それぞれ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では高階関数の使用は許されていない。
筆者が開発している音楽のためのプログラミング言語mimium(MInimal-Musical-medIUM)\cite{Matsuura2021}では、ラムダ計算に遅延とフィードバックを基本構文として組み込むことで、汎用プログラミング言語と同様の構文を維持しながら、信号処理を簡潔に表現することができる。ただ、mimiumのこれまでの問題として、意味論が厳密に定義できていないために、遅延やフィードバックを含む内部状態を伴う関数と、再帰関数や高階関数の組み合わせを含むコードをコンパイルできないという点があった。 筆者が開発している音楽のためのプログラミング言語mimium(MInimal-Musical-medIUM)\cite{Matsuura2021}では、ラムダ計算に遅延とフィードバックを基本構文として組み込むことで、汎用プログラミング言語と同様の構文を維持しながら、信号処理を簡潔に表現することができる。ただ、mimiumのこれまでの問題として、意味論が厳密に定義できていないために、遅延やフィードバックを含む内部状態を伴う関数と、再帰関数や高階関数の組み合わせを含むコードをコンパイルできないという点があった。
本稿では、W-calculusの2つの制限非線形システムと高階関数を緩和する形で設計された、mimiumの中間表現となる計算モデル$\lambda_{mmm}$を提案する\footnote{本稿の内容は、2024年11月に行われたInternational Faust Conference 2024での筆者の発表\cite{matsuura2024}を、対象読者として日本語話者かつ言語設計コミュニティを想定し大幅に改稿したものである。}。以降の節では、まず第2節で$\lambda_{mmm}$のシンタックスと型付け規則、ナイーブな操作的意味論を簡単に紹介する。この操作的意味論は現実的に直接リアルタイム実行することが難しいため、実際のmimiumの実行環境では、$\lambda_{mmm}$を実際に実行するための仮想機械(VM)とその命令セットを持つ。第3章ではこのVMおよび命令セットの設計について概説する。第4節では、現状の$\lambda_{mmm}$を下敷きにしたmimiumの設計の今後の課題および展望を3つの視点で議論する。1つ目は、計算がグローバル環境の評価時に発生するのか、実際の信号処理中に発生するのかをユーザーが区別しなければならないという問題である。2つ目は、if式による部分的な内部状態更新を許すかどうかが信号処理の表現できる範囲およびユーザー体験に大きな影響を与えるという点である。3つ目は、$\lambda_{mmm}$の中間表現を取り入れることで外部関数の呼び出しが容易になるという点である。 本稿では、W-calculusの2つの制限非線形システムと高階関数を緩和する形で設計された、mimiumの中間表現となる計算モデル$\lambda_{mmm}$を提案する\footnote{本稿の内容は、2024年11月に行われたInternational Faust Conference 2024での筆者の発表\cite{matsuura2024}を、対象読者として日本語話者かつ言語設計コミュニティを想定し大幅に改稿したものである。}。以降の節では、まず第2節で$\lambda_{mmm}$のシンタックスと型付け規則、ナイーブな操作的意味論を簡単に紹介する。この操作的意味論は現実的に直接リアルタイム実行することが難しいため、実際のmimiumの実行環境では、$\lambda_{mmm}$を実際に実行するための仮想機械(VM)とその命令セットを持つ。第3章ではこのVMおよび命令セットの設計について概説する。第4節では、現状の$\lambda_{mmm}$を下敷きにしたmimiumの設計の今後の課題および展望を3つの視点で議論する。1つ目は、計算がグローバル環境の評価時に発生するのか、実際の信号処理中に発生するのかをユーザーが区別しなければならないという問題である。2つ目は、if式による部分的な内部状態更新を許すかどうかが信号処理の表現できる範囲およびユーザー体験に大きな影響を与えるという点である。3つ目は、$\lambda_{mmm}$の中間表現を取り入れることで外部関数の呼び出しが容易になるという点である。
@@ -111,22 +111,17 @@ let\ & onepole = \\
典型的な単純型付きラムダ計算に追加される $\lambda_{mmm}$ の型付け規則を図\ref{fig:typing}に示す。プリミティブ型には、ほとんどの信号処理で使われる実数型と、ディレイの添字などに使われる自然数型がある。$\lambda_{mmm}$ の設計に直接影響を与えたW-calculusでは、関数型は実数のタプルを取り、実数のタプルを返すことしかできない。これはすなわち高階関数の定義を制限していることを意味する。高階関数は実行時にクロージャのような動的メモリ割り当てに依存するデータ構造を必要とするので、この制約は信号処理言語として合理的でな一方で、ラムダ計算の汎用性を失わせてしまってもいる。 典型的な単純型付きラムダ計算に追加される $\lambda_{mmm}$ の型付け規則を図\ref{fig:typing}に示す。プリミティブ型には、ほとんどの信号処理で使われる実数型と、ディレイの添字などに使われる自然数型がある。$\lambda_{mmm}$ の設計に直接影響を与えたW-calculusでは、関数型は実数のタプルを取り、実数のタプルを返すことしかできない。これはすなわち高階関数の定義を制限していることを意味する。高階関数は実行時にクロージャのような動的メモリ割り当てに依存するデータ構造を必要とするので、この制約は信号処理言語として合理的でな一方で、ラムダ計算の汎用性を失わせてしまってもいる。
$\lambda_{mmm}$ では、クロージャのメモリ割り当ての問題はランタイム側の設計と実装に先送りすることで(第\ref{sec:vm}節を参照)、高階関数の使用を許している。しかし、$feed$の抽象化では、関数型を含まない型\footnote{タプルを含む合成型を扱う場合、単純な関数型のみならずメンバに関数型を1つも含まない型を意味する。}のみを許す。$feed$抽象で関数型を許可するということは、例えば1サンプル前の関数から現在の時刻に使用する関数を合成する、といった記述を可能にすることになる。しかしこれを可能にすると、クロージャによる動的メモリ割り当ての問題に加えて、時間経過ごとに新しいクロージャが過去のクロージャを参照し続けることで使用するメモリサイズが肥大化する空間漏洩space leakという更なる問題が発生する。このため、現在mimiumでは専ら実装を簡易化するという理由で関数型を含む型を$feed$の項では取れないようにしている。 $\lambda_{mmm}$ では、クロージャのメモリ割り当ての問題はランタイム側の設計と実装に先送りすることで(第\ref{sec:vm}節を参照)、高階関数の使用を許している。しかし、$feed$の抽象化では、関数型を含まない型\footnote{タプルを含む合成型を扱う場合、単純な関数型のみならずメンバに関数型を1つも含まない型を意味する。}のみを許す。$feed$抽象で関数型を許可するということは、例えば1サンプル前の関数から現在の時刻に使用する関数を合成する、といった記述を可能にすることになる。しかしこれを可能にすると、クロージャによる動的メモリ割り当ての問題に加えて、時間経過ごとに新しいクロージャが過去のクロージャを参照し続けることで使用するメモリサイズが肥大化する空間漏洩space leakという更なる問題が発生する\footnote{空間漏洩は関数型リアクティブプログラミングFRPという、時変値の伝播を関数合成で記述するパラダイムの初期の実装の問題点として現れた\cite{krishnaswami2013}。近年の実用的なFRPライブラリや言語ではこの問題の解決に様々なアプローチで取り組んでいるため、今後の時間変化する関数型の値の活用の可能性はありえる。}。このため、現在mimiumでは専ら実装を簡易化するという理由で関数型を含む型を$feed$の項では取れないようにしている。
% todo:frpとの関連について補足
\subsection{ナイーブな操作的意味論} \subsection{ナイーブな操作的意味論}
\label{sec:semantics} \label{sec:semantics}
\input{semantics.tex} \input{semantics.tex}
\ref{fig:semantics}$\lambda_{mmm}$ のビッグステップ(評価の中間過程を考えず、最終的な評価結果のみを考える)操作的意味論の抜粋を示す。この意味論では各時刻にごとに評価環境が用意され、現在時刻が $n$ における、 $t$ サンプル過去の評価環境を $E^{n-t}$呼ぶ。0より小さい時刻の評価環境が参照された場合は、どの項もその型のデフォルト値数値型の場合は0として評価される。 \ref{fig:semantics}$\lambda_{mmm}$ のビッグステップ(評価の中間過程を考えず、最終的な評価結果のみを考える)操作的意味論の抜粋を示す。この意味論では各時刻にごとに評価環境が用意され、現在時刻が $n$ における、$t$ サンプル過去の評価環境を $E^{n-t}$表記する。0より小さい時刻の評価環境が参照された場合は、どの項もその型のデフォルト値数値型の場合は0として評価される。
この意味論で$\lambda_{mmm}$ を実際の計算機上で評価しようとすると、各サンプルごとに時刻0から現在時刻までを再計算し、各ステップですべての変数環境を保存する必要がある。しかし実際には、 $delay$$feed$ が使用する内部メモリ空間を考慮した仮想マシンが定義され、項は実行前にこのマシン用の命令にコンパイルされる。 この意味論で$\lambda_{mmm}$ を実際の計算機上で評価しようとすると、各サンプルごとに時刻0から現在時刻までを再計算し、各ステップですべての変数環境を保存する必要がある。しかし実際には、 $delay$$feed$ が使用する内部メモリ空間を考慮した仮想マシンが定義され、項は実行前にこのマシン用の命令にコンパイルされる。
\section{VMおよび命令セットの設計} \section{VMおよび命令セットの設計}
\label{sec:vm} \label{sec:vm}
@@ -134,9 +129,9 @@ $\lambda_{mmm}$ を実行するための仮想マシン(VM)モデルとその命
ラムダ計算をベースとした計算モデルを実行する際の重要な課題は、クロージャと呼ばれるデータ構造を扱うことである。クロージャは、入れ子になった関数を定義した場所での変数環境をキャプチャし、外部関数のコンテキストにある変数を参照できるようにする。例えばクロージャを内側の関数の定義とそこで使われる変数と値の辞書との対として定義すれば、コンパイラ(もしくはインタプリタ)の実装は簡単だが、実行時の性能は制限される。 ラムダ計算をベースとした計算モデルを実行する際の重要な課題は、クロージャと呼ばれるデータ構造を扱うことである。クロージャは、入れ子になった関数を定義した場所での変数環境をキャプチャし、外部関数のコンテキストにある変数を参照できるようにする。例えばクロージャを内側の関数の定義とそこで使われる変数と値の辞書との対として定義すれば、コンパイラ(もしくはインタプリタ)の実装は簡単だが、実行時の性能は制限される。
逆に、クロージャ変換(ラムダ・リフティング)と呼ばれる処理を使えば、実行時のパフォーマンスを向上させることができる。この処理では、内部関数が参照するすべての外部変数を解析し、内部関数の隠し引数として追加するような変換を行う。しかし、コンパイラでのこの変換の実装は比較的複雑になる。 逆に、クロージャ変換(ラムダ・リフティング)と呼ばれる、内部関数が参照するすべての外部変数を解析し、内部関数の隠し引数として追加するような処理を使えば、実行時のパフォーマンスを向上できる。しかし、コンパイラでのこの変換の実装は比較的複雑になる。
LuaのVMは、2つの方法の中間的なアプローチである上位値\textit{upvalue})という概念を採用しており、命令 \texttt{GETUPVALUE}\texttt{SETUPVALUE} を追加することで、実行時に外部変数を動的に参照できるようになっている。上位値を使ったコンパイラとVMの実装は、クロージャの完全な変換よりも単純でありながら、大幅な性能低下を避けることができる。このアプローチでは、クロージャが元の関数のコンテキストをエスケープしない限り、外部変数はヒープメモリではなくコールスタックを介してアクセスされる\cite{nystrom2021}。さらに、upvalueは他のプログラミング言語との相互運用性を促進する。例えばLua用の外部ライブラリをC言語で実装する場合、プログラマはC API経由でランタイムのコールスタック上の値だけでなく、Luaランタイムのupvalueにアクセスすることもできる。 LuaのVMは、2つの方法の中間的なアプローチである上位値\textit{upvalue})という概念を採用しており、命令 \texttt{GETUPVALUE}\texttt{SETUPVALUE} を追加することで、実行時に外部変数を動的に参照できるようになっている。上位値を使ったコンパイラとVMの実装は、クロージャの完全な変換よりも単純でありながら、大幅な性能低下を避けることができる。このアプローチでは、クロージャが元の関数の文脈からエスケープしない限り、上位値はヒープメモリではなくコールスタックへの間接的参照で読み取られる\cite{nystrom2021}。さらに、上位値の利用は他のプログラミング言語との相互運用性を促進する。例えばLua用の外部ライブラリをC言語で実装する場合、プログラマはC API経由でランタイムのコールスタック上の値だけでなく、Luaランタイムのupvalueにアクセスすることもできる。
\subsection{命令セット} \subsection{命令セット}
\label{sec:instruction} \label{sec:instruction}
@@ -146,19 +141,19 @@ LuaのVM命令と $\lambda_{mmm}$ のVM命令は以下の点で異なる。
\begin{enumerate} \begin{enumerate}
\item mimiumはLuaと異なり静的型付け言語であるため、基本的な算術演算の命令が型ごとに用意されている。 \item mimiumはLuaと異なり静的型付け言語であるため、基本的な算術演算の命令が型ごとに用意されている。
\item 静的型付けと状態付き高階関数を管理するために、呼び出し操作が通常の関数呼び出しとクロージャ呼び出しに分割されている(詳細は\ref{sec:vmstructure}を参照)。 \item 静的型付けと状態付き高階関数を管理するために、呼び出し操作が通常の関数呼び出しとクロージャ呼び出しに分割されている(詳細は\ref{sec:vmstructure}を参照)。
\item 条件文\texttt{JMP}\texttt{JMPIFNEG} の2つの命令を組み合わせて実装されているが、Lua VM では専用の \texttt{TEST} 命令が採用されている \item if文のような条件式にはLua VMでは専用の \texttt{TEST} 命令が採用されているが、mimiumで\texttt{JMP}\texttt{JMPIFNEG} の2つの命令を組み合わせて実装されている。
\item forループに関連する命令、オブジェクト指向プログラミングで使用される\texttt{SELF}命令、変数へのメタデータ参照に関連する\texttt{TABLE}命令は不要なため、mimiumでは省略している。 \item forループに関連する命令、オブジェクト指向プログラミングで使用される\texttt{SELF}命令、変数へのメタデータ参照に関連する\texttt{TABLE}命令は不要なため、mimiumでは省略している。
\item リスト、タプル、配列のようなデータ構造の実装は、本稿で示す $\lambda_{mmm}$ の記述の範囲外であるため除外している\footnote{実際のコンパイラでは、タプルのようなデータ型は、型に応じてアドレスのオフセットを事前に計算し、単純な型に対する命令として分解されているため、ランタイム上でタプル用の特別な命令は持っていない。} \item リスト、タプル、配列のようなデータ構造の実装は、本稿で示す $\lambda_{mmm}$ の記述の範囲外であるため除外している\footnote{実際のコンパイラでは、タプルのようなデータ型は、型に応じてアドレスのオフセットを事前に計算し、単純な型に対する命令として分解されているため、ランタイム上でタプル用の特別な命令は持っていない。}
\end{enumerate} \end{enumerate}
$\lambda_{mmm}$ のVMは、Lua VMバージョン5以降と同様にレジスタマシンとして動作する。ただし、通常のレジスタマシンとは異なり、物理レジスタは使うわけではない。命令内でのレジスタ番号は単に、VM実行中のベースポインタに対するコールスタック上のオフセットを指す。またほとんどの命令の最初のオペランドは、演算結果が格納されるレジスタ番号を指定する。 $\lambda_{mmm}$ のVMは、Lua VMバージョン5以降と同様にレジスタマシンとして動作する。ただし、命令内でのレジスタ番号は物理レジスタを指すわけではなく、単に、VM実行中のベースポインタに対するコールスタック上のオフセットを指す。またほとんどの命令の最初のオペランドは、演算結果が格納されるレジスタ番号を指定する。
\input{instructions.tex} \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} と表現される。 命令の一覧を図\ref{fig:instruction}に示す(基本的な算術演算などは省略した。命令の表記は、Lua VMのドキュメントに概説されている形式(\cite[p.13]{ierusalimschy2005}に従った。各命令は32ビットの整数として表現される\footnote{命令の種類に8ビットを割り当てているのは後の拡張のためでもあるが、実装の際Rustのタグ付き共用体(\texttt{enum})として実装するのが簡単なためという理由も大きい。}。図\ref{fig:instruction}の擬似コードでは左から順に8ビットずつ、命令の種類、最大3つのオペランドのリストが並ぶ。3つのオペランドがそれぞれ符号なし8ビット整数として使用される場合、\texttt{A B C}のように表現される。オペランドが符号付き整数として使用される場合は、接頭辞 \texttt{s} が付く。複数のオペランドフィールドを組み合わせて16ビット、24ビットの値にする場合、接尾辞 \texttt{x}\texttt{xx} が付加される。例えば、\texttt{B}\texttt{C} を結合して符号付き 16 ビット値として扱う場合、\texttt{sBx} と表現される。
擬似コードでは、\texttt{R(A)} は現在の関数のベースポインタ + \texttt{A} にあるレジスタ(またはコールスタック)に出し入れされるデータを示す。\texttt{K(A)}はコンパイル済みプログラムの静的変数セクションの \texttt{A} 番目のエントリを指し、\texttt{U(A)}は現在の関数の \texttt{A} 番目のupvalueにアクセスする。 擬似コードでは、\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つの新しい演算が導入されている。 Luaのupvalue演算に加えて、$delay$$feed$ 式のコンパイルを処理するために、 \texttt{GETSTATE}\texttt{SETSTATE}\texttt{SHIFTSTATE}\texttt{DELAY}の4つの新しい演算が導入されている。
@@ -170,20 +165,19 @@ Luaのupvalue演算に加えて、$delay$ と $feed$ 式のコンパイルを処
\caption{\label{fig:vmstructure}{$\lambda_{mmm}$を実行するための仮想機械、プログラム、またインスタンス化されたクロージャの概要を表した図。}} \caption{\label{fig:vmstructure}{$\lambda_{mmm}$を実行するための仮想機械、プログラム、またインスタンス化されたクロージャの概要を表した図。}}
\end{figure*} \end{figure*}
\ref{fig:vmstructure}に、 $\lambda_{mmm}$ のVM、プログラム、および実行中にインスタンス化されたクロージャの関係を示す。通常のコールスタックに加えて、VMは、フィードバックと遅延のための内部状態データを管理する専用の記憶領域単純な1次元の配列、以下内部状態ストレージおよび複数の状態ストレージを切り替えるための状態スタック後述を持つ。
\ref{fig:vmstructure}に、 $\lambda_{mmm}$ のVM、プログラムと、 実行中にインスタンス化されたクロージャの関係を示す。通常のコールスタックに加えて、VMは、フィードバックと遅延のための内部状態データを管理する専用の記憶領域単純な配列を持つ この内部状態ストレージには、\texttt{GETSTATE}命令と\texttt{SETSTATE}命令で内部ステートデータを取得する位置を示すポインタが付随している。これらの位置は、\texttt{SHIFTSTATE}命令を使用して前方または後方に移動される。状態記憶メモリ内の実際のデータレイアウトは、コンパイル時に \texttt{self}\texttt{delay}、およびその他の状態関数を含む関数呼び出しを解析して静的に決定される。\texttt{DELAY}操作は2つの入力として\texttt{B}は入力される信号、\texttt{C}は遅延時間を受け取る
この記憶領域には、\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は内部状態ストレージのポインタを状態スタックからポップする。
しかし、高階関数--別の関数を引数として受け取ったり、1つの関数を返したりする関数--では、渡された関数の内部状態のメモリレイアウトはコンパイル時には不明である。そのため、インスタンス化されたクロージャは、VMインスタンスが保持するグローバルな内部状態記憶領域とは別の内部状態記憶領域が割り当てられる。また、VMはインスタンス化されたクロージャの内部状態記憶域のポインタを追跡するために、コールスタックとは別に状態ストレージポインタを記録するスタック以下、\textbf{状態スタック}と呼ぶ)を使用する。\texttt{CALLCLS}操作が実行されるたびに、VMはクロージャの内部状態記憶のポインタを状態スタックにプッシュする。クロージャの呼び出しが完了すると、VMは内部状態記憶ポインタを状態スタックからポップする。 インスタンス化されたクロージャは、それ自身の上位値(upvalue)用の記憶領域も保持する。クロージャが親関数のコンテキストにいる「オープンなクロージャ」とここでは呼ぶ限り、その上位値は、現在のスタックのベースポインタから負のオフセットを保持する。このオフセットはコンパイル時に決定され、プログラム内の関数のプロトタイプに格納される。さらに、上位値はローカル変数だけでなく、親関数の上位値を参照することもあるこの状況は少なくとも3つの関数が入れ子になっている場合に発生する。つまり、値が親関数のローカルなスタック変数か、さらなる上位値かを示すタグと、それに対応するインデックススタックの負のオフセットまたは親関数の上位値インデックスである。
インスタンス化されたクロージャは、それ自身の上位値(upvalue)用の記憶領域も保持する。クロージャが親関数のコンテキストにいる「オープンなクロージャ」とここでは呼ぶ限り、その上位値は、現在のスタックのベースポインタから負のオフセットを保持する。このオフセットはコンパイル時に決定され、プログラム内の関数のプロトタイプに格納される。さらに、上位値はローカル変数だけでなく、親関数の上位値を参照することもあるこの状況は少なくとも3つの関数が入れ子になっている場合に発生する。つまり、値が親関数のローカルなスタック変数かさらなる上位値かを示すタグと、それに対応するインデックススタックの負のオフセットまたは親関数の上位値インデックスである。
例えば、プログラム中のupvalueインデックスが\texttt{\[upvalue(1), local(3)\]}と指定されている場合を考える。この場合、\texttt{GETUPVALUE 6 1} という命令は、(\texttt{upvalue(1)}によって参照される)上位値一覧のインデックス \texttt{3} にある値を、ベースポインタに対して相対的に \texttt{R(-3)} から取得し、その結果を \texttt{R(6)} に格納することを示す。 例えば、プログラム中の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の実装では参照カウント方式でメモリを開放している。} クロージャが \texttt{RETURN} 命令によって元の関数コンテキストから抜け出すとき、事前に挿入された \texttt{CLOSE} 命令によってアクティブな上位値はスタックからヒープメモリに退避させられる。これらの上位値は、特にネストしたクロージャを含む場合には、複数の場所から参照される可能性がある。したがって、これらの上位値が使用されなくなった時にはガベージコレクションでメモリを解放する必要がある\footnote{現在のmimiumの実装では参照カウント方式でメモリを開放している。}
$\lambda_{mmm}$ は値呼びラムダ計算であり、再代入が存在しないので、 \texttt{SETUPVALUE}命令は省略される。再代入が許可されているような意味論を追加する場合、オープンなものも含めupvalueは共有メモリセルとして実装される必要がある。 $\lambda_{mmm}$ は値呼びラムダ計算であり、再代入が存在しないので、 \texttt{SETUPVALUE}命令は省略される。再代入が許可されているような意味論を追加する場合、オープンなものも含めupvalueは共有メモリセルとして実装される必要がある\footnote{現在のmimiumの実装では、初期実装からの使用を引き継いで\texttt{let}で宣言された変数に対する再代入が許されているため、実際には\texttt{SETUPVALUE}も使用されているが、本稿では議論を単純化するため省略した。関数に値を渡す際に値はコピーされるため、関数の外側の状態を変更する方法はクロージャがキャプチャした変数への再代入のみとなっている。}
\subsection{VM命令へのコンパイル例} \subsection{VM命令へのコンパイル例}
@@ -205,7 +199,7 @@ fn onepole(x,g) state_size:1
SETSTATE 2 // store to self SETSTATE 2 // store to self
RETURN 3 1 RETURN 3 1
\end{verbatim} \end{verbatim}
\caption{\label{fig:bytecode_onepole}{\ref{fig:onepole-code}の単極フィルターのコードをVM命令にコンパイルした際の例。}} \caption{\label{fig:bytecode_onepole}{\ref{fig:onepole-code}の単極フィルターのコードをVM命令にコンパイルした結果を擬似コードで表した例。}}
\end{figure} \end{figure}
\ref{fig:bytecode_onepole}は図\ref{fig:onepole-code}で示したmimiumでの単極フィルターのコードをVMの命令列にコンパイルした結果の例である。\texttt{self}が参照されると、\texttt{GETSTATE}命令で値を取得し、\texttt{SETSTATE}命令で戻り値を格納して内部状態を更新してから、\texttt{RETURN}命令で値を返す。この場合、実際の戻り値は2番目の \texttt{GETSTATE} 命令を使用して取得され、時刻0の時にはデフォルトの値数値型なら0を返すようになる。 \ref{fig:bytecode_onepole}は図\ref{fig:onepole-code}で示したmimiumでの単極フィルターのコードをVMの命令列にコンパイルした結果の例である。\texttt{self}が参照されると、\texttt{GETSTATE}命令で値を取得し、\texttt{SETSTATE}命令で戻り値を格納して内部状態を更新してから、\texttt{RETURN}命令で値を返す。この場合、実際の戻り値は2番目の \texttt{GETSTATE} 命令を使用して取得され、時刻0の時にはデフォルトの値数値型なら0を返すようになる。
@@ -282,7 +276,7 @@ fn dsp (x)
\begin{figure}[ht] \begin{figure}[ht]
\centerline{\includegraphics[width=0.7\hsize]{fbdelay_spos.pdf}} \centerline{\includegraphics[width=0.7\hsize]{fbdelay_spos.pdf}}
\caption{\label{fig:fbdelay_spos}{\it\ref{fig:bytecodes_fbdelay}の命令列を実行している間の、内部状態ストレージ上を移動する状態位置ポインタの移動過程を概念的に示した図。}} \caption{\label{fig:fbdelay_spos}{\it\ref{fig:bytecodes_fbdelay}\texttt{twodelay}関数の命令列を実行している間の、内部状態ストレージ上を移動する状態位置ポインタの移動過程を概念的に示した図。}}
\end{figure} \end{figure}
\ref{fig:fbdelay_code} と 図\ref{fig:bytecodes_fbdelay}に、より複雑なコードの例とそのコンパイル結果のVM命令列の例を示した。このコードでは\texttt{fbdelay}としてフィードバック付きのディレイ関数を定義して、別の関数\texttt{twodelay}では異なる引数を使う2つのフィードバック付きディレイを呼び出す。さらに、 \texttt{dsp}\texttt{twodelay} 関数を2つ使う。 \ref{fig:fbdelay_code} と 図\ref{fig:bytecodes_fbdelay}に、より複雑なコードの例とそのコンパイル結果のVM命令列の例を示した。このコードでは\texttt{fbdelay}としてフィードバック付きのディレイ関数を定義して、別の関数\texttt{twodelay}では異なる引数を使う2つのフィードバック付きディレイを呼び出す。さらに、 \texttt{dsp}\texttt{twodelay} 関数を2つ使う。
@@ -319,7 +313,7 @@ fn dsp(){
さらに別の例として図\ref{fig:filterbank_good}に高階関数\texttt{filterbank}を使うものを示す。この関数は、別の関数\texttt{filter}(これは入力と周波数を引数として受け取る)を引数として受け取り、\texttt{filter}のインスタンスを\texttt{n}個複製し、それらを足し合わせるものである\footnote{以前のmimiumの仕様\cite{Matsuura2021}では、変数束縛と破壊的代入の構文は同じ(\texttt{x = a})だった。しかし、新しいバージョンの構文では、変数の束縛に\texttt{let}キーワードを使用している。} さらに別の例として図\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} 命令が実行される際に、使用する内部状態記憶領域が動的に切り替わるためである。 以前の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] \begin{figure}[ht]
\centering \centering
@@ -411,8 +405,7 @@ mimiumでは時間の経過とともに変化する内部状態を持つ関数
しかしmimiumでは、評価の段階が グローバル環境の評価(信号処理グラフの具体化)および \texttt{dsp} 関数の繰り返しの実行内部状態の暗黙の更新を伴う実際の信号処理の2つに分かれている。 しかしmimiumでは、評価の段階が グローバル環境の評価(信号処理グラフの具体化)および \texttt{dsp} 関数の繰り返しの実行内部状態の暗黙の更新を伴う実際の信号処理の2つに分かれている。
例に挙げたmimiumのコードには破壊的な代入は含まれていないが、図\ref{fig:filterbank_good}では、グローバル環境の評価中に\texttt{filterbank}関数の再帰的な実行が1回だけ発生する。逆に、コード\ref{fig:filterbank_bad}では、\texttt{dsp}関数が毎サンプル実行されるたびに再帰的な関数が実行され、クロージャが生成される。クロージャの内部状態はクロージャのアロケーション時に初期化されるため、図\ref{fig:filterbank_bad}の例では、\texttt{filterbank}が評価された後クロージャの内部状態は毎時刻リセットされてしまうことになる。 例に挙げた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とは対照的に、この多段階計算の記述は信号処理グラフの生成と信号処理の実行の両方で統一された意味論を保持している。 この問題に対処する方法として、グローバル環境評価ステージ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とは対照的に、この多段階計算の記述は信号処理グラフの生成と信号処理の実行の両方で統一された意味論を保持している。
@@ -441,16 +434,16 @@ fn dsp(){
クロージャのデータは、図\ref{fig:vmstructure}で示したように、関数と内部状態の組み合わせで表現されている。\verb|filterbank|の例で内部状態に対して特別な操作を必要としていないということは、mimiumからCやC++で書かれた発振器やフィルターなどのUGenを、通常のクロージャと同じように呼び出すことができることを意味する。さらに、外部UGenをパラメトリックに複製・合成することも可能になる。この機能はFaustや類似の言語では実装が難しいが、 $\lambda_{mmm}$ の設計では簡単に実現できる。 クロージャのデータは、図\ref{fig:vmstructure}で示したように、関数と内部状態の組み合わせで表現されている。\verb|filterbank|の例で内部状態に対して特別な操作を必要としていないということは、mimiumからCやC++で書かれた発振器やフィルターなどのUGenを、通常のクロージャと同じように呼び出すことができることを意味する。さらに、外部UGenをパラメトリックに複製・合成することも可能になる。この機能はFaustや類似の言語では実装が難しいが、 $\lambda_{mmm}$ の設計では簡単に実現できる。
ただし、現在mimiumはサンプル単位の処理を基本にしており、バッファ単位の信号を扱うことができない。ほとんどのネイティブに実装されたUGenはバッファ単位でデータを処理するため、今のところ、既存の外部UGenが使用可能な実用的ケースは多くない。しかしその上で、厳密にサンプル単位の処理を必要とするのは$feed$ の項のみであるため、一度に1サンプルしか処理できない関数と、一括して複数サンプルを処理する関数とを型レベルで区別することは可能なはずである。Faustでマルチレートの仕様が検討されているように、バッファ単位で処理できる部分をコンパイラが自動的に判断することで、外部ユニットジェネレータ間とバッファ単位での連携も可能かもしれない。 ただし、現在mimiumはサンプル単位の処理を基本にしており、バッファ単位の信号を扱うことができない。ほとんどのネイティブに実装されたUGenはバッファ単位でデータを処理するため、今のところ、既存の外部UGenが使用可能な実用的ケースは多くない。しかしその上で、厳密にサンプル単位の処理を必要とするのは$feed$ の項のみであるため、一度に1サンプルしか処理できない関数と、一括して複数サンプルを処理する関数とを型レベルで区別することは可能なはずである。Faustでマルチレートの仕様が検討されているように、バッファ単位で処理できる部分をコンパイラが自動的に判断することで、外部UGen間とバッファ単位での連携も可能かもしれない。
\section{結論} \section{結論}
\label{sec:conclusion} \label{sec:conclusion}
本稿では、音楽・信号処理用プログラミング言語の中間表現 $\lambda_{mmm}$ と、それを実行するための仮想マシン・命令セットを提案した。 $\lambda_{mmm}$ は、信号処理グラフの生成と、その実際の処理を統一された構文と意味論で記述可能にする。ただし、関数がグローバル環境で評価されるかDSPの繰り返し実行で評価されるかの判別はユーザーの責任となり、初学者にはその区別が難しいという欠点がある。 本稿では、音楽・信号処理用プログラミング言語の中間表現 $\lambda_{mmm}$ と、それを実行するための仮想マシン・命令セットを提案した。 $\lambda_{mmm}$ は、信号処理グラフの生成と、その実際の処理を統一された構文と意味論で記述可能にする。ただし、関数がグローバル環境で評価されるかDSPの繰り返し実行で評価されるかの判別はユーザーの責任となり、初学者にはその区別が難しいという欠点がある。
また本論文では、VMの動作を記述する擬似コードとに加えて、mimiumでのコードの例とそれに対応するバイトコードの例を示すことでコンパイル過程を説明したのみである。より正式な意味論と詳細なコンパイル過程の提示は、今後多段階計算の導入も踏まえ検討する必要がある。 また本論文では、VMの動作を記述する擬似コードとに加えて、mimiumでのコードの例とそれに対応するバイトコードの例を示すことでコンパイル過程を説明したのみである。この過程を表示的意味論として形式化する作業は、現在反映されてない合成型の形式化に加えて今後多段階計算の導入と並行して検討する必要がある。
この研究が、コンピュータ上での音・音楽のより一般的な表現に貢献し、音楽のための言語理論とプログラミング言語理論のより広範な分野との間のより深いつながりを促進することを期待する。 この研究が、コンピュータ上での音・音楽のより一般的な表現に貢献し、音楽のための言語理論とプログラミング言語理論のより広範な分野との間のより深いつながりを促進することを期待する。\\
\begin{acknowledgment} \begin{acknowledgment}

View File

@@ -310,3 +310,17 @@
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.}, 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} file = {/Users/tomoya/Zotero/storage/KFYY25CM/Taha, Sheard - 1997 - Multi-Stage Programming with Explicit Annotations.pdf;/Users/tomoya/Zotero/storage/X3DDM6HN/full-text.pdf}
} }
@inproceedings{krishnaswami2013,
title = {Higher-Order Functional Reactive Programming without Spacetime Leaks},
booktitle = {Proceedings of the 18th {{ACM SIGPLAN}} International Conference on {{Functional}} Programming},
author = {Krishnaswami, Neelakantan R.},
year = {2013},
month = sep,
pages = {221--232},
publisher = {ACM},
address = {Boston Massachusetts USA},
doi = {10.1145/2500365.2500588},
urldate = {2024-11-29},
isbn = {978-1-4503-2326-0},
language = {en}
}

View File

@@ -32,11 +32,11 @@
\end{equation*} \end{equation*}
\textrm{} \textrm{}
\end{minipage}\\ \end{minipage}\\
\multicolumn{2}{l}{ \multicolumn{2}{c}{
\begin{minipage}[t]{0.4\hsize} \begin{minipage}[t]{0.4\hsize}
\begin{equation*} \begin{equation*}
\begin{aligned} \begin{aligned}
&e&::=& \; x \;\; (x \in {v_p}) & [value] &\\ &e&::=& \; x & [variable] &\\
& & |& \; \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] &\\ & & |& \; let\; x = e_1\; in\; e_2 & [let] &\\
@@ -50,9 +50,9 @@
%%|& \quad \textasciitilde e \quad & [escape] %%|& \quad \textasciitilde e \quad & [escape]
\end{aligned} \end{aligned}
\end{equation*} \end{equation*}
\centering\textrm{}
\end{minipage} \end{minipage}
} }
\end{tabular} \end{tabular}
\centering \textrm{} %置き方おかしいけどこれで上手くレイアウトされてるのでこのまま
\caption{\label{fig:syntax_v}{\it$\lambda_{mmm}$の値、型、項の定義の抜粋。基本演算などは省略した。}} \caption{\label{fig:syntax_v}{\it$\lambda_{mmm}$の値、型、項の定義の抜粋。基本演算などは省略した。}}
\end{figure} \end{figure}