モジュールシステムの意味論と実装について考える
必要要件
理想としては、以下を全部満たしたい
- 分割コンパイルしたうえで、コンパイル済みのモジュールは変更がなければ再利用できる
- 操作的意味論的にもある程度一貫性がある
- モジュール内ではシンボルの相互参照が可能(後から定義されるシンボルでも参照可能)
分割コンパイルができるということは、依存するファイルを毎回コンパイルしないでも、型情報のキャッシュが残ってれば再利用できるということ
バラバラのMIRとしてコンパイルできて、関数や定数はmonomorphizeされたユニークな名前:値の対応関係が1:1で付く
デザインチョイスのトレードオフとしては意味論を楽にしようと思うと分割コンパイルがめんどくさくなる
複数のコードを合体させるときに、流用されるコードの複製が増える問題
ただ、どっちみちDSPにおけるコードの量なんてたかが知れているのでは?という問題も
実際、そこまでモジュール内での相互参照って必要か?というのもある、Letrecで十分じゃね?
モジュールの循環参照が起きたらどうなるのかという問題もある
モジュール内での宣言一覧
//関数のvisibilityセッティングはRustとおなじ感じ<!-- line:31 -->
pub fn (){<!-- line:32 -->
<!-- line:33 -->
}<!-- line:34 -->
pub const Foo = 100<!-- line:35 -->
pub type Bar = Constructor(()->float) //新しい型宣言<!-- line:36 -->
pub type alias Hoge = ()->float //エイリアス宣言<!-- line:37 -->
<!-- line:38 -->シンタックス:モジュールの宣言
mod modname{//ソースファイルと同じ宣言<!-- line:44 -->
//toplevel_decls<!-- line:45 -->
}<!-- line:46 -->mod modname{//ステージの変更<!-- line:50 -->
#stage = macro<!-- line:51 -->
//<!-- line:52 -->
}<!-- line:53 -->- モジュール単位でMIRとバイトコードを生成して、あとからリンクできるようにする
- ただし、ステージ0マクロの展開もしないといけないので、ASTも出力して保持していないといけない
モジュールを値としてレコード型に型付けできると話が早いんだけど、それ一級モジュールの機能だよな
意味論
stage(1)<!-- line:66 -->
mod(0) modname{<!-- line:67 -->
//toplevel_decls<!-- line:68 -->
fn foo()-> `float {<!-- line:69 -->
...<!-- line:70 -->
}<!-- line:71 -->
}<!-- line:72 -->
modname::foo!()<!-- line:73 -->これがあったとすると、
extern modname = ${<!-- line:78 -->
foo = | | { },<!-- line:79 -->
...<!-- line:80 -->
}<!-- line:81 -->
<!-- line:82 -->
modname.foo!()<!-- line:83 -->
<!-- line:84 -->こういう感じかなー、あ、でもこうするとモジュール内での相互参照が解決できないか
トップレベルでのモジュールの名前解決は後から定義されたシンボルも参照できるので、型推論のやり方はなんか考える必要がありそう
一旦トップレベルの宣言の名前だけを回収して、あとから実際の定義の型推論を行っていく形になる
しかし、そうするとラムダ計算として定義する旨味はあんまりないのでは、という気がしてくる
extern name : Type という宣言に変換さえできれば意味論を保ってコンパイルはできそう
型がつけられさえすればいいから、そこで循環する定義にならなければOK
//modA.mmm<!-- line:100 -->
use modB<!-- line:101 -->
pub fn hoge(){<!-- line:102 -->
modB::hoge()<!-- line:103 -->
}<!-- line:104 -->
<!-- line:105 -->
//modB.mmm<!-- line:106 -->
use modA<!-- line:107 -->
pub fn hoge(){<!-- line:108 -->
modA::hoge()<!-- line:109 -->
}<!-- line:110 -->(ランタイムで無限ループするけどそれはいいとして)変換するとこう
// modA.mmm<!-- line:116 -->
extern modB:{<!-- line:117 -->
hoge: ?<!-- line:118 -->
}<!-- line:119 -->
fn hoge(){<!-- line:120 -->
modB.hoge()<!-- line:121 -->
}<!-- line:122 -->
<!-- line:123 -->
//modB.mmm<!-- line:124 -->
extern modA:{//実際は型コンストラクタとかで覆って区別できるようにすべき<!-- line:125 -->
hoge: ?<!-- line:126 -->
}<!-- line:127 -->
fn hoge(){<!-- line:128 -->
modA.hoge()<!-- line:129 -->
}<!-- line:130 -->modA::hoge : 'a ()->'a, modB::hoge : 'a ()->'a
ここまでしか型は決定できないということに
ただまあ、型が決定不能ならその時コンパイルエラーにすればいいだけで、一応型推論自体は無限ループに陥らず完了するのか?ただ、extern宣言に変えるためには一回すべてのモジュールを読み込んでみてからでないと名前一覧すら作れない
Rustの場合はグローバルな宣言が基本的に型推論しなくて済むようになっているからなあ
型付け手順
- モジュールA読み込み開始
- いったんuse宣言以外の全部のProgramStatementを読んで名前をグローバル空間に登録(型宣言は関数などわかるところ以外はすべて変数として登録)
- モジュールー型宣言マップにファイル名登録
- use modBの解釈開始
- マップに問い合わせ、ファイルがないのでファイル読み込み開始
- use modAの解釈開始
- マップに問い合わせ、ファイルがあったのでそれを参照
- とりあえずmodAの型はすべて不明なレコード型として解釈して型付け開始
- Program→Exprへの変換の中でuse宣言をextern modA:{…}に変換し、名前空間参照をレコードへのアクセスに変換
両方が推論終わってから再度Unificationしないといけないか?これも再帰を禁止しておけばそれでいいのか
---
Program = FunctionDefinition<!-- line:167 -->
|GlobalDeclaration<!-- line:168 -->
|ModuleDeclaration<!-- line:169 -->
|use ModuleName<!-- line:170 -->
ModuleDeclaration = Visibility? mod (StageDeclaration)? Identifier { Program }<!-- line:171 -->
<!-- line:172 -->
Visivility : pub <!-- line:173 -->
<!-- line:174 -->
Pythonのモジュールシステム Python’s Import System - Module object|Regular/Namespace Packages|Finders & Loaders|Relative imports - YouTube
モジュールはオブジェクトであり、__file__などはモジュールオブジェクトに付属するメンバ変数
Gluonのモジュール Modules - Gluon Documentation
これマクロとして実装してあって、それこそレコードとしてファイルが埋め込まれて出力されるだけなので、分割コンパイルとかモジュール内相互参照とかは全然考慮されてないけど意味論はすっきり
一応このやり方でも、ファイルに対する型情報さえ残ってればキャッシュと分割コンパイル自体は可能?
問題点:レコードとして埋め込むと、エクスポートできる関数はステージ0かステージ1のいずれかになる(モジュール内でステージの混在自体はできるけど) なんかこれだとダメだというもっとはっきりした理由をこの前気がついた気がするんだが、思い出せねえ、、