The WapL Programming Language 日本語版(Compiler ver)
著: kazanefu
本ドキュメントではコンパイラ版WapL 0.1.5かそれ以降を前提としています。WapLコンパイラのインストールやアップデート方法は第1章のWapLを導入
はじめに
コンパイラ版WapLはシンプルなルールで記述で低レベルの制御まででき、コンパイル時間・実行時間ともに高速で、非常に簡易的な所有権/借用の仕組みによってコンパイル時にある程度のメモリ安全を保障することができ、また主にC/C++や他にもLLVMバックエンドの言語とリンクすることのできる言語です。著者が個人で趣味で作ったレベルの言語なので高レベルな記述やライブラリの量などは非常に少なくなってしまっていますが、前向きに捉えれば簡単に習得することができる言語でもあります。
この本の使い方
この本では章の順に読むことを想定した構成にはなっていますが、読み飛ばしてわからないところがあったら戻ってみるなど自由に読んでもらって構いません。自分に合った方法でどうぞ。
第1章ではWapLのインストール方法からwapl-cliの使い方まで紹介します。
第2章ではWapLにおける変数や関数や演算や制御フローといったプログラミングにおける基本的な概念について説明します。
第3章ではWapLでの非常に簡易的な所有権/借用の仕組みとメモリ管理について説明します。
第4章では構造体について説明します。
第5章では複数ファイルでの開発と標準ライブラリの使い方について軽く説明します。
第6章では第5章までに分類されなかった説明を細かく分けて説明します。
WapLを始める
WapLへようこそ、この章では以下のことを説明します。
- LinuxにWapLコンパイラをインストールする
Hello, world!と表示するプログラムを書くwapl-cliを使用する
インストール
まず、Githubのreleaseからインストーラを起動してwaplupとwapl-cliをインストールします。その後、waplupを使ってコンパイラwaplcをインストールします
Linuxにインストールする
注:WapLでは内部でClangを使うので事前にインストールしておいてください。もし関数がリンクできない等のエラーが出る場合はCのライブラリが不足している可能性がありますので確認してください。WapLではClangは2025/12/7現在21を推奨しています
bashで以下のコードを実行してインストーラを呼んでください
$ curl -fsSL https://github.com/kazanefu/WapL_Compiler/releases/latest/download/installer.sh | bash
成功していれば以下のような出力がされるでしょう:
Installing WapL toolchain...
Installation complete!
Please reload your shell: source ~/.bashrc
Example: waplup install latest
Recommendation: if you need syntax highlighting for VScode, visit here https://github.com/kazanefu/WapL_SyntaxHighLight/releases/
Require: if you don't have Clang installed yet, install it.
続いてwaplupを使ってwaplcをインストールします。バージョンを指定してインストールしたい場合は以下のコマンドのlatestをバージョンの数字に置き換えてください
$ waplup install latest
成功していれば以下のように出るはずです(これはversion 0.1.5をインストールした場合の例です):
Installed version 0.1.5
トラブルシューティング
waplcが正常にインストールされているか確認するには以下のコマンドを実行してください:
$ waplc --version
正常にインストールされていれば以下の形式でデフォルトに設定されているバージョンが表示されるはずです。
waplc x.y.z
複数のバージョンを管理とアンインストール
waplup経由でwaplcをインストールした場合簡単にバージョンの切り替えやアンインストールができます。 waplを最新のバージョンに更新したい場合は以下のコマンドを実行してください:
$ waplup update
waplcのデフォルトのバージョンを変更するには以下の形式でバージョンを指定してください:
$ waplup default x.y.z
インストール済みのwaplcのバージョンを確認するには以下のコマンドを実行してください:
$ waplup list
現在のデフォルトのバージョンを確認するには以下のコマンドを実行してください:
$ waplup show
アンインストールする場合は以下のようにバージョンを指定して特定のバージョンのみをアンインストールするか,allを指定してwaplupやwapl-cliも含めてすべてアンインストールすることができます:
$ waplup uninstall x.y.z
$ waplup uninstall all
Hello, World!
伝統に従ってHello, Worldを出力する簡単なコードを書いてみましょう
プロジェクトのディレクトリを作成する
まずはWapLコードを格納するディレクトリを作るところから始めましょう。WapLではどこにプロジェクトを作成するかはたいした問題ではありませんが、この本ではホームディレクトリにprojectsディレクトリを作成してプロジェクトをすべてそこに保管することを推奨します。
projectsディレクトリを作成して,その中にhello_worldディレクトリを作成してこれを「Hello, World!」のプロジェクトのディレクトリとします。
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
WapLプログラムを書いて実行する
次にソースファイルを作り、main.waplというファイルにしてください。WapLのファイルは常に.waplという拡張子で終わります。
作成したら、main.waplファイルを開き次のコードを入力してください。
ファイル名:main.wapl
fn main():i32{
println("Hello, World!");
return 0s;
}
main.waplファイルを保存したhello_worldディレクトリにいることを確認して以下のコマンドを打ってファイルをコンパイルして実行してください:
$ waplc -i main.wapl -o main
$ ./main
Hello, World!
Hello, World!という文字列が出力できたはずです。この出力が見れない場合は「WapLを導入」に戻ってもう一度手順に漏れや間違いがなかったかを確認してみてください。
Hello, World!が確かに出力されたら、おめでとうございます!これであなたもWapLプログラマーです!ようこそ!
WapLプログラムの解説
この「Hello, World!」プログラムを詳しく再確認しましょう。こちらが一つ目の要素です:
fn main():i32 {
}
これらの行ではmainという名前の関数を定義しています。WapLではmainは特別な関数で実行可能なWapLプログラムで走る最初のコード、すなわちエントリーポイントになります。ただし、多くの他の言語とは違い、main関数が存在しない場合でもエントリーポイントを任意の関数に指定することができます。引数がある場合は()の内部に入ります。その後に続く:i32は戻り値の型を指定しています。WapLのmain関数は常にi32という32bit整数の型の戻り値です。
関数の本体は{}に囲まれています。
main関数の本体は、こんなコードになっています:
println("Hello, World!");
return 0s;
WapLでは複数行にわたる括弧の中身はインデントをするのが基本です。また,式の終わりは;で終わるのが原則です。これらは守らなくても正常に動作はしますが、可読性やコードの意味を明示的に示すために守ることを推奨します。
printlnは与えられた文字列を標準出力に出力する関数です。ここでは引数に"Hello, World!"を渡しています。
returnは戻り値を返しています。returnはほとんどのコードを関数呼び出し形式で記述するWapLの中では珍しくreturn 0sのように半角スペースで区切って戻り値を書きます。ここでは特に意味はないですが0sを返しています。0sはi32型の0のリテラルですが詳しくは第2章で説明します。
コンパイルと実行は別ステップ
先ほど打った以下のコマンドの意味について説明します。
$ waplc -i main.wapl -o main
$ ./main
WapLコンパイラ版ではwaplcに-iでソースファイルを、-oで出力する実行ファイルを指定して実行可能ファイルを出力します。そして、二つ目のコマンドで一つ目のコマンドで出力した実行可能ファイルを実行しています。
PythonやRuby、JavaScriptなどの動的言語に造詣が深いなら、プログラムのコンパイルと実行を個別の手順で行うことに慣れていない可能性があります。WapLコンパイラ版はAOTコンパイル(ahead-of-time;訳注:予め)言語です。つまり、実行可能ファイルを誰かにあげ、受け取った人がWapLをインストールしていなくても実行できるわけです。.py、.rb、.jsファイルをあげたら、それぞれPython、Ruby、JavaScriptの処理系がインストールされている必要があります。
簡単なプロジェクトならwaplcでコンパイルするだけでも十分ですが、wapl-cliを使うことでもっと簡単にプロジェクトの作成、ビルド、標準ライブラリなどをできるようになります。次は、wapl-cliについて紹介します。
wapl-cliを使う
wapl-cliはWapLプロジェクトの作成、標準ライブラリの取得、ビルド/リリースビルドを簡単にすることができるCLIツールです。これによってプロジェクトの開始からリリースまでより簡単にできるようになります。以降はwapl-cliを使用することを想定しています。まずはwapl-cliが使える状態にあるか確認してください。
$ wapl-cli --help
wapl-cli commands:
new <name>
build
std_load
run
このようにwapl-cliのコマンド一覧が出てきたら成功です。もし失敗したら場合は「WapLを導入」に戻ってインストーラを呼んでください。
wapl-cliでプロジェクトを作成する
wapl-cliを使って新しいプロジェクトを作成しましょう。まずはprojectsディレクトリに戻ってください。ここではとりあえず名前をhello_wapl_cliという名前でプロジェクトを作成します。
$ wapl-cli new hello_wapl_cli
$ cd hello_wapl_cli
これによってhello_wapl_cliという名前のプロジェクトを作成できました。hello_wapl_cliディレクトリの中は以下のようになってるはずです。
.
├── src
│ └── main.wapl
├── std
│ ├── HelloWorld.wapl
│ ├── String.wapl
│ ├── VecT.wapl
│ ├── allstd.wapl
│ ├── assign_and_cal.wapl
│ ├── iterator.wapl
│ ├── longinput.wapl
│ ├── math.wapl
│ ├── sort.wapl
│ ├── time.wapl
│ └── utility.wapl
├── target
└── wapl.toml
srcにmain.waplがあり、main.waplには以下のコードが書かれています。
fn main():i32{ println("Hello, WapL!"); return 0s; }
また、stdには標準ライブラリが入っています。
注釈:上記の標準ライブラリの中身はバージョン0.1.5時点のものです。バージョンによって内容物が異なってる場合があります。
また、targetというディレクトリも作られています。ここにはwapl-cliを使ってビルドした実行ファイルが保存される場所になります。
そしてwapl.tomlにはビルド設定が書かれています。中身は以下のようになっています:
[build]
input = "src/main.wapl"
output = "target/hello_wapl_cli"
opt = "O2"
clang = "clang"
[release]
input = "src/main.wapl"
output = "target/hello_wapl_cli"
opt = "O3"
clang = "clang"
waplc_v0.2.16以降に付属するwapl-cliを使った場合では[wasm]の欄が追加されて、さらに[build]/[release]にもbitsizeという項目が追加されているはずです。
wapl-cliでビルド/実行する
hello_wapl_cliディレクトリにいることを確認して以下のコマンドでmain.waplのコードをビルドします。
$ wapl-cli build
LLVM IR output: ./src/main.ll
Build success! → ./target/hello_wapl_cli
Build complete: ./target/hello_wapl_cli
のように出れば成功です。場合によっては以下のように警告が出ることがありますが動作には問題ありません。
warning: overriding the module target triple with xxx [-Woverride-module]
1 warning generated.
ビルドしたことで./target/hello_wapl_cliにプロジェクトの名前で実行可能ファイルが作られ、./src/main.llに中間言語のLLVM IRのファイルが生成されます。もし他の言語で作ったものでWapLで作ったものとリンクさせたい場合はこのIRを使うことができます。
以下のように実行することができます。
$ ./target/hello_wapl_cli
Hello, WapL!
先ほどwapl-cli buildでビルドし、./target/hello_wapl_cli で実行しましたが、wapl-cli runでこの二つを一つのコマンドですることもできます。
$ wapl-cli run
LLVM IR output: ./src/main.ll
Build success! → ./target/hello_wapl_cli
Build complete: ./target/hello_wapl_cli
Hello, WapL!
リリースに向けてビルドする場合はwapl-cli releaseを使います。これにより、ClangのO3相当の最適化をした状態でビルドをすることができますが、wapl-cli buildと比較してビルドには時間が長くなるため、wapl-cli buildは開発時に素早く頻繁に再ビルドしたいとき用で、wapl-cli releaseは最終的なビルドのとき用です。
wapl-cliはたいていの場合waplcを直接使うより便利ですが、内部で使用するClangの指定や実行ファイルを作らずにIRのみ作成、デバッグ用にASTをすべて出力するなどの細かい設定をしたい場合はwaplcを直接使う必要があります。
一般的なプログラミングの概念
この章では、ほとんどの言語で見られる概念の説明と、WapLにおけるそれらの動作について見ていきます。概念は多くの言語で同じものを共有しますが、記述方法や細かい挙動は異なるものも多いです。これらの概念のWapLにおける仕様の説明をします。
具体的には、変数、基本的な型、関数、コメント、制御フローについて説明します。
変数
WapLコンパイラ版では変数の宣言時に型を明示的に記述する必要があり、宣言時にスタック上にメモリ確保をします。また、初期値の代入は宣言と同時に行う必要はありませんが、値を読み取るまでには代入をしてください
fn main():i32{
#=(x,10,i64);
println(format("%d",x));
=(x,5);
println(format("%d",x));
#=(y,_,i64);
=(y,0);
println(format("%d",y));
return 0s;
}
実行すると以下のような出力が得られます。
10
5
0
このように変数の宣言は#=(変数名,初期値,型)の形式で行います。また、初期値なしで宣言する場合は_を初期値の入れます。また、代入は=(変数,値)という形式で記述します。
fn main():i32{
#=(x,_,i64);
println(format("%d",x));// 代入されてないので0が出力される.
return 0s;
}
このように値を代入せずに読み取るとエラーは出ませんが思わぬ値が渡される可能性があるので必ず読み取る前に値を代入するようにします。
宣言に伴う初期値は一部例外を除いて型が変数の型と一致する必要があります。以下のようなプログラムはxをi64の型で宣言しているにも関わらず、初期値が'5'とchar型になっているためコンパイル時にエラーが出ます。
fn main():i32{
#=(x,'5',i64);
return 0s;
}
Error: x Type miss match : expected Ident("i64") found Ident("char")
シャドーイング
内側のスコープで外側にあるのと同じ名前で新たに変数を宣言すると、前の変数は新しい変数に覆い隠され、そのスコープより内側では新しい変数の値が現れます。また、そのスコープを抜ければ再び前の変数の値が現れるようになります。
fn main():i32{
#=(x,5,i64);
#=(x,+(x,5),i64);
loopif:inner(true){
#=(x,*(x,2),i64);
println(format("The value of x in the inner scope is: %d",x));
warpto(break-inner);
}
println(format("The value of x is: %d",x));
return 0s;
}
The value of x in the inner scope is: 20
The value of x is: 10
このコードではまずxを5という値に束縛します。それから#=(x,...をもう一度行い、xをもとの値に5を足した値、つまり10で覆い隠します。次にスコープの中に入って#=で3回目の宣言でさらに元の値の2倍の20で覆い隠します。ここで、xを読み取るともちろん20です。スコープを抜けると3回目の宣言でのシャドーイングが終了するためxを読み取ると2回目の宣言による10が現れます。
基本的な型
コンパイラ版WapLは静的型付き言語であり、コンパイル時にすべての変数の型が判明している必要があります。基本的に型は変数や関数の宣言時に明示的に書く必要があり、それ以降は型推論が効きます。
スカラー型
スカラー型は、単独の値を表します。WapLにおいては主に4つのスカラー型があります: 整数、浮動小数点数、論理値、文字、です。
整数型
WapLでは2種類の整数型があります:i32とi64とisizeです。それぞれ32-bit、64-bitの符号ありの整数、ビルド時に何ビットアーキテクチャをターゲットにしているかに合わせたサイズになる符号あり整数です。符号ありなので例えばi32だと$-(2^{31})$から$2^{31}-1$まで表現できます。
整数リテラルは小数点を含まないただの整数はi64になり、末尾にsをつけるとi32になります。また末尾に_をつけるとisizeになります
| 型 | 例 |
|---|---|
| i64 | -123 |
| i32 | 42s |
| isize | 10_ |
浮動小数点型
整数型と同様に浮動小数点型にも32-bitと64-bitのものがあります:f32とf64です。
浮動小数点リテラルは小数点を持つ数値です。また、浮動小数点リテラルも整数リテラル同様に32-bitならsを末尾につけ、つけなければf64として扱われます。
| 型 | 例 |
|---|---|
| f64 | -123.45 |
| f32 | 42.0s |
数値演算
WapLにも全数値型に期待されうる標準的な数学演算が用意されています: 足し算、引き算、掛け算、割り算、余りです。WapLで特徴的なのは異なる型で数値演算を行った際には左側の型に合わせて自動でキャストされるということです。
fn main():i32{
// 足し算
#=(sum, +(5,20.2), i64);// 25
// 引き算
#=(difference, -(10.5,3), f64);// 7.5
// 掛け算
#=(product, *(42,5), i64);// 210
// 割り算
#=(quotient, /(49.54,10.0), f64);// 4.954
#=(floored, /(2,3), i64);// 0
// 余り
#=(remainder, %(43,5), i64);// 3
return 0s;
}
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして、変数に束縛されます。
論理値型
WapLの論理値型:boolはtrueとfalseの二つの値をとることができます。
fn main():i32{
#=(t, true, bool);
#=(f, false, bool);
return 0s;
}
文字型
WapLで文字はchar型で扱います。charは8-bitであるためASCIIまでしか扱えません。また、文字リテラルはシングルクォーテーションで囲みます。
fn main():i32{
#=(c1, 'A', char);
#=(c2, '\x41', char);
#=(c3, '\u{41}',char);
println(format("%c",c1));
println(format("%c",c2));
println(format("%c",c3));
return 0s;
}
A
A
A
これらはすべてAを表します。
ポインタ型
WapLでは配列や文字列などもポインタ型であらわします。ポインタ型には4種類あり、ptr:T *:T &:T &mut:Tがあり、Tのところにポインタが指す型が入ります。ポインタのポインタだったりする場合はptr:ptr:Tのようになります。それぞれのポインタ型の詳しい説明は第3章 簡易的な所有権/借用でします。
fn main():i32{
#=(str, "hello", ptr:char);
println(format("%s",str));
#=(arr, salloc(i64,5), ptr:i64);
=(arr, Array(3,1,4,1,5));
#=(i,0,i64);
loopif:(<(i,5)){
println(format("%d", [](arr,i)));
=(i,+(i,1));
}
#=(x, 10, i64);
#=(p, &_(x), ptr:i64);
println(format("%d", *_(p)));
return 0s;
}
hello
3
1
4
1
5
10
このように、文字列や配列もポインタ型として扱います。文字列リテラルはダブルクォーテーションで囲みます。また、配列は先に型と要素数を指定してsallocでスタック上にメモリを確保してArrayで値を列挙して配列に格納していき、[](ポインタ変数, インデックス)で値を取り出せます。[]では多次元配列でも[](ポインタ変数, インデックス1, インデックス2, ...)のようにして簡単に値を取り出すことができます。
固定長配列型(0.2.16以降)
固定長配列型はすべての要素が同じ型である必要があり、また配列のサイズを変更することもできません。これは型名自体がサイズと要素の型の情報を持っているためです。例えば要素の型がi64でサイズが10の固定長配列型の型名はarray_10:i64となるように、要素の型T、サイズNに対してarray_N:Tという型名になります。またarray(1,2,3)のようにして値を作ることができます。要素へのアクセスはポインタでは[]を用いましたが、固定長配列型では[array]を用います。先ほどのポインタ型による配列の処理を固定長配列型でも同じように書いてみましょう。
fn main():i32{
#=(str, array('h','e','l','l','o'), array_5:char);
println(format("%s",as(str,ptr:char)));
#=(arr,array(3,1,4,1,5) , array_5:i64);
#=(i,0,i64);
loopif:(<(i,5)){
println(format("%d", [array](arr,i)));
=(i,+(i,1));
}
return 0s;
}
hello
3
1
4
1
5
固定長配列型のとポインタ配列の最も大きな違いは固定長配列型は要素そのものが値であるという点です。ポインタ配列の値は0個目の要素の値があるメモリ上のアドレスであり、それを他の変数に渡しても参照するアドレスが渡されるだけで要素の値が格納されている場所は同じです。一方固定長配列型では要素そのものがすべて渡されるため要素が格納されている場所は変数によって異なります。
fn print_array(ptr:i64 arr){
#=(i,0,i64);
loopif:(<(i,7)){
print(format("%d,", [](arr,i)));
=(i,+(i,1));
}
println("");
}
fn main():i32{
// ====固定長配列型====
println("固定長配列型");
#=(arr1,array(0,1,1,2,3,5,8) , array_7:i64);
#=(arr2, arr1, array_7:i64);
// arr2のインデックス3の値を99に書き変える
=([array](arr2,3),99);
// ----arr1を表示----
print_array(as(arr1,ptr:i64));
// ----arr2を表示----
print_array(as(arr2,ptr:i64));
// ====ポインタ配列型====
println("ポインタ配列");
#=(ptr1, salloc(i64,7), ptr:i64);
=(ptr1,Array(0,1,1,2,3,5,8));
#=(ptr2, ptr1, ptr:i64);
// ptr2のインデックス3の値を99に書き変える
=([](ptr2,3),99);
// ----ptr1を表示----
print_array(ptr1);
// ----ptr2を表示----
print_array(ptr2);
return 0s;
}
固定長配列型
0,1,1,2,3,5,8,
0,1,1,99,3,5,8,
ポインタ配列
0,1,1,99,3,5,8,
0,1,1,99,3,5,8,
そのため、このようにしたときに固定長配列型ではarr2に加えた変化はarr1には影響がありませんが、ポインタ配列ではptr2に加えた変化がptr1にも反映されます。
関数
WapLでは1.2で述べたように基本的にmain関数がエントリーポイントになります。しかし、main以外の関数をエントリーポイントのようにすることも可能です。
fn start(){
println("Hello, Start!");
}
start();
これは内部的には一番最後のstart()の呼び出しが_TOPLEVEL_という見えない関数に入れられて、_TOPLEVEL_が最初に呼ばれて関数の外に書いてある処理を順に行うのでstart関数が呼ばれています。
WapLでは関数は必ず関数の呼び出しより上で関数の定義がされている必要があります。
fn main():i32{
another();
return 0s;
}
fn another():void{
println("Hello, from another function!");
}
Function another not found
のようにエラーがでます。これを正しく動くようにするためには以下のような順で書く必要があります。
fn another():void{
println("Hello, from another function!");
}
fn main():i32{
another();
return 0s;
}
または宣言だけを先に置くことでも解決できます:
declare another():void;
fn main():i32{
another();
return 0s;
}
fn another():void{
println("Hello, from another function!");
}
このように関数の定義はfn、宣言はdeclareで行います。
関数の引数
引数は関数のシグネチャの一部になる特別な変数のことで、宣言時には実引数は作らなくてよく、()の中に型のみを,区切りで列挙し、関数の宣言のときは型と変数名を書きます。
declare another_function(i64,i64);
fn main():i32{
another_function(5,3)
return 0s;
}
fn another_function(i64 x,i64 y){
println(format("%d,%d",x,y));
}
戻り値のある関数
引数を書いた括弧の後ろに戻り値がある場合は:で区切ってその後に戻り値の型を書きます。またreturnで戻り値を返します。:
fn five():i64{
return 5;
}
fn main():i32{
println(format("five() return %d",five()));
return 0s;
}
ここではfive関数の戻り値の型はi64でreturnで5を返しています。declareで宣言をするときも同様です。
declare five():i64;
fn main():i32{
println(format("five() return %d",five()));
return 0s;
}
fn five():i64{
return 5;
}
コメント
WapLでコメントアウトは//でそれ以降のその行をコメントにするものしかありません。コメントはコンパイラは無視しますが、ソースコードを読む人間にとっては有益なものと思えるでしょう。
// ここではコメントについて説明するよ!
// コメントは各行ごとに//を入れる必要があるよ
fn main():i32{
// 下のようにコードの書かれた行の末尾をコメントにすることもできるよ
#=(ans, 42, i64);// すべての答え
return 0s
}
制御フロー
point/warpto文
WapLで最も基本となる制御フローはpoint/warpto文です。これは多くの言語でのgoto文に近い概念で非常に自由度の高い処理が書けます。pointでラベルをつけてブロックを作り、warptoでそこまでジャンプします。warptoは
fn main():i32{
warpto(skip);
point skipped;
println("この行は飛ばされるよ");
warpto(skip);
point skip;
println("skipに飛んだよ");
return 0s;
}
skipに飛んだよ
これではskippedとskipというラベルでブロックを作り、warpto(skip)でskipに飛びます。すべてのブロックがreturnかwarpto/warptoifのどれかで終わる必要があります。
warptoif文
warptoifはwarptoに条件を付けて真のときと偽のときで飛ぶ先を選択するものでwarptoif(条件, 真のとき, 偽のとき)となります。
fn main():i32{
#=(cond, true, bool);
warptoif(cond, then, else);
point then;
println("then");
warpto(end);
point else;
println("else");
warpto(end);
point end;
return 0s;
}
このようにして条件分岐を作ることもできますし、後述するloopifも内部ではpoint/warpto/warptoifと同じようなことがされています。
loopif文
loopifは他の多くの言語で存在するwhileのようなもので、名前を付けたりwarpto/warptoifにも対応しています。
fn main():i32{
#=(i,0,i64);
loopif:(<(i,10)){
println(format("%d",i))
=(i,+(i,1));
}
=(i,0)
loopif:LOOP1(<(i,10)){
println(format("LOOP1:%d",i))
=(i,+(i,1));
warptoif(<(i,5),continue-LOOP1,break-LOOP1);
}
return 0s
}
0
1
2
3
4
5
6
7
8
9
LOOP1:0
LOOP1:1
LOOP1:2
LOOP1:3
LOOP1:4
このようにloopifは名前ありでも名前なしでも作ることができ、ループを抜けたいときはbreak-名前に飛ぶことで抜けることができ、以下の処理をせずに条件の評価に戻りたいときはcontinue-名前に飛ぶことでできる。
if文(0.1.13以降)
多くの言語で見られるif文。言語によってはif文とif式は同じ文法で記述できるものもありますが、WapLでは異なるキーワードを使っています。
if文は以下のように使うことができます
fn main():i32{
if (true){
println("True");
}
if (false){
println("False");
}
return 0s;
}
True
このようにifのあとの()の中にbool型で条件を書き、trueのときには{}の中の処理が実行されます。
elifとelse
if文で前の条件がfalseであるときにfalse側でも処理をしたいときに毎回ifで前の条件がfalseであるときを指定するのはめんどくさいです。そこで使えるのがelifとelseです。
fn main():i32{
#=(x, 7, i64);
if (>(x, 10)){
println(format("%lld > 10",x));
}elif (>(x, 5)){
println(format("5 < %lld <= 10",x));
}else{
println(format("%lld <= 5",x));
}
return 0s;
}
5 < 7 <= 10
このように前の条件がfalseであることを前提にして条件を書いたり、その他の場合を記述できます。
?演算子
これはif式に相当する演算子です。中には文は書けないのでもし文を書きたい場合は関数を作ってその関数を呼ぶようにしてください。
fn main():i32{
#=(x, ?(false,10,5), i64);
println(format("x = %lld", x));
return 0s;
}
x = 5
このように第1引数に条件を書き、続けてtrueのときの値、falseのときの値を書きます。このとき値の型は一致していてさらにvoidであってはいけません。
fn Hello(){
println("Hello");
}
fn Bye(){
println("Bye");
}
fn main():i32{
?(true,Hello(),Bye());
return 0s;
}
src/main.ll:56:1: error: expected instruction opcode
56 | if.merge: ; No predecessors!
| ^
1 error generated.
このようにvoidの場合エラーが出ます。これを回避するためには以下のようにして意味がなくとも戻り値を持たせる必要があります。
fn Hello():i32{
println("Hello");
return 0s;
}
fn Bye():i32{
println("Bye");
return 0s;
}
fn main():i32{
?(true,Hello(),Bye());
return 0s;
}
Hello
choose
?演算と似たようなものとしてchooseがあります。違いとしては?は片方しか評価しない一方chooseは両方を評価してから条件に合う値を返します。先ほどのHelloとByeの?をchooseに書き変えて試してみましょう。
fn Hello():i32{
println("Hello");
return 0s;
}
fn Bye():i32{
println("Bye");
return 0s;
}
fn main():i32{
choose(true,Hello(),Bye());
return 0s;
}
Hello
Bye
このようにHello/Byeともに実行されたことが確認できます。
簡易的な所有権/借用
WapLの所有権はRustの所有権を参考にしてより簡易的で緩いチェックではあるものの、ある程度の安全性をコンパイル時に担保できます。この章では、所有権の挙動とポインタの使い方について話していきます。
WapLでの簡易的な所有権
所有権と聞いたらまずRust言語を思い浮かべる人が多いでしょう。WapLでもRustの所有権を参考にしてとても簡易的なものではありますが所有権によってメモリリークや多重解放をコンパイル時に検出してエラーを出せます。ただし、WapLではRustの所有権よりもチェックは極めて緩く、例えばメモリ競合や多次元配列のメモリリークなどは防ぐことはできません。また、メモリの確保と解放は自動では行われずC言語のように明示的に記述する必要もあります。とはいえ、明示的に所有者や借用やムーブを記述するため、プログラマはメモリ安全を意識してコーディングができるでしょう。WapLの簡易的な所有権はRustのともまた少し異なるため慣れるまでに少し時間がかかるかもしれませんが、慣れてしまえばガベージコレクションのようなランタイム処理によるオーバーヘッドなしで安全なコードを書くことができます。
所有権規則
所有権のルールは以下の通りです
- ヒープ上にある値に対して所有者であるポインタがただ一つ存在する。
- 所有者はスコープを抜けるまでに解放または所有権の譲渡がされていなければいけない。
変数スコープ
スコープとは、変数が有効な範囲のことです。基本的にWapLでは{}で囲まれた範囲が一つのローカルスコープになります。
fn main():i32{ // aはまだ有効ではない
#=(a, 10, i64); // aが有効になる
loopif:Scope(true){ // sはまだ宣言されていないため有効ではない
#=(s, "hello", ptr:char); // ここでsが有効になる
warpto(break-Scope);
} // スコープが終わりsは無効になる
return 0s; // スコープが終わりaが無効になる
}
コメントで書いたようにスコープを抜けるまではそのスコープの中で宣言された変数は有効で、スコープを抜けたときに無効になります。
メモリ確保
WapLには2種類のメモリ確保があります。必要なサイズのメモリをヒープ領域から動的に取ってくるmallocとメモリをスタック領域に確保するsallocがあります。スタック確保の場合は関数を抜けると自動的に解放され、返り値として関数の外に持ち出すことはできません。一方、ヒープ確保の場合はfreeで解放する必要があり、所有権のチェックの対象です。
fn main():i32{
#=(sz, sizeof(i64), i64); // i64の型のバイト数
#=(h, malloc(*(sz,10),i64), *:i64); // ヒープ確保してi64のポインタに割り当てる
#=(s, salloc(i64,10), ptr:i64); // スタック確保してi64のポインタに割り当てる
free(pmove(h)); // ヒープ確保したhを解放する
return 0s;
}
mallocのときはサイズは変数でいいですが、sallocのときはサイズがi64リテラルである必要があります。また、ここでそれぞれ型名が*:i64とptr:i64で異なる理由は次に説明します。
4種類のポインタ型
第2章の基本的な型で少し触れたとおりWapLには4種類のポインタがあります:ptr:T *:T &:T &mut:Tです。
| 型 | 説明 |
|---|---|
| ptr | 所有権のチェックを一切受けず、スタック上のメモリやより自由な操作がしたいときの用いる |
| * | ヒープ確保される所有者ポインタで、値に対してただ一つのみ存在し、スコープを抜けるまでにpmoveで譲渡される必要がある |
| & | 不変借用ポインタで値の書き変えとpmoveが禁止されている |
| &mut | 可変借用ポインタで値の書き変えはできるがpmoveは禁止されている |
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(owner, malloc(*(sz,10),i64), *:i64); // 所有者
=(owner, Array(1,2,3,4,5,6,7,8,9,0)); // 値を入れる
#=(immut, p&(owner), &:i64); // 不変借用
#=(mutable, p&mut(owner), &mut:i64); // 可変借用
=([](mutable,5),10); // &mutは書き変え可能
#=(i,0,i64)
loopif:(<(i,10)){
println(format("%d",[](owner,i)));
=(i,+(i,1));
}
#=(owner2, pmove(owner), *:i64); // 所有権をownerからowner2に譲渡し以降ownerは使えない
free(pmove(owner2)); // 所有者を解放
return 0s;
}
所有権の譲渡:ムーブ
先ほどのポインタ型の説明での#=(owner2, pmove(owner), *:i64)のように所有権を譲渡した後、前の所有者は無効になり、アクセスしようとするとエラーが出ます。
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(owner, malloc(*(sz,10),i64), *:i64); // 所有者
#=(owner2, pmove(owner), *:i64); // 所有権をownerからowner2に譲渡し以降ownerは使えない
free(pmove(owner)); // 前の所有者を解放しようとしているためエラー
free(pmove(owner2));
return 0s;
}
Error:"owner" already moved. it is prohibited to read moved pointer
Error : at pmove "owner" already moved
これにより二重解放を防ぐことができます。
所有者の責任
所有者はスコープを抜けるまでに所有権を譲渡するか解放する必要があります。
fn main():i32{
#=(sz, sizeof(i64), i64)
#=(p, malloc(*(sz,10),i64), *:i64); // 所有者
return 0s; // まだ譲渡も解放もしてない
}
Error:you need to free or drop pointer p!
このようにエラーが出ることでメモリリークを防げます。
借用と参照
先ほどのポインタ型の説明でもあったようにWapLではp&やp&mutで同じ場所を指す所有権を持たないポインタを作ることができます。WapLではこれを借用と呼んでいます。Rustでは関数の引数に参照を取ることを借用と呼んでいますがWapLでは少し異なり、借用はポインタから作ります。WapLにもRustのように&_(値を持つ変数)で参照を生成することもでき、これはその変数の値が格納されてるアドレスを指すポインタを返しています。また、実際はp&やp&mutはほとんど何もしておらず、受け取る側の型によってそのポインタをどのポインタ型として扱うかを決定しており、どのような形でポインタを渡しているのかを明示的に書くようにしているだけです。
参照の逆は参照外しであり、*_(ポインタ変数)または*_(ポインタ変数,返す型)のように記述します。
fn get_at(&:i64 arr,&mut:i64 i):i64{
=(*_(i), -(*_(i),1));
return [](arr, *_(i));
}
fn main():i32{
#=(sz,sizeof(i64),i64);
#=(array, malloc(*(sz,5),i64), *:i64);
=(array, Array(3,1,4,1,5));
#=(indx, 2, i64);
println( format("[](array,-(2,1)) = %d\nindex = %d",get_at( p&(array), &_(indx) ), indx ) );
free(pmove(array));
return 0s;
}
[](array,-(2,1)) = 1
index = 1
このように&:i64で渡すことで所有権を譲渡せずに参照を渡すことができます。また、indxは参照渡しでget_at関数に渡されているため、そこでの値の変化がmain関数にも反映されています。
構造体
構造体を定義する
構造体を定義
構造体の定義はstructキーワードのあとに構造体の名前を付け、波かっこの中にフィールドを型名、名前の順で書いていきます。以下に複素数を表現する構造体の例を示します。
struct Complex{
f64 re,
f64 im
}
reには実部、imには虚部を格納します。
インスタンスを生成
定義した構造体名は型名としても使えて、その型名で変数を宣言することで実体を持ったインスタンスを生成します。また、これだけではフィールドに値が入ってないのでそれぞれ値を代入していく必要があります。
struct Complex{
f64 re,
f64 im
}
fn main():i32{
#=(c, _, Complex);
=(.(c,re),1.0);
=(.(c,im),2.0);
println(format("c = %g + %gi",.(c,re),.(c,im)));
return 0s
}
c = 1 + 2i
フィールドへのアクセスは.(インスタンス,フィールド名)でできます。
しかし、これを毎回やっていてはコードが長くなってしまいます。そこで、フィールドの値を引数で渡して一括で代入してくれる関数を作ると便利です。
fn Complex_new(f64 re,f64 im):Complex{
#=(cplx, _, Complex);
=(.(cplx,re), re);
=(.(cplx,im), im);
return cplx;
}
このような関数を用意しておくと
#=(c, Complex_new(1.0,2.0), Complex);
とするだけで先ほどのようにComplexのインスタンスを簡単に生成できます。
フィールドにアクセス
先ほども述べたように構造体のフィールドへのアクセスは.(インスタンス,フィールド名)でできますが、構造体はポインタで扱うことも多いので、インスタンスのポインタを使ってアクセスする方法が二つあります:_>(インスタンスのポインタ,フィールド名)と->(インスタンスのポインタ,フィールド名)です。_>はインスタンスのポインタからフィールドの値を返し、->はインスタンスのポインタからフィールドのポインタを返します。
| アクセス方法 | 第一引数 | 戻り値 |
|---|---|---|
| . | インスタンスの値 | フィールドの値 |
| _> | インスタンスのポインタ | フィールドの値 |
| -> | インスタンスのポインタ | フィールドのポインタ |
構造体での所有権
フィールドが所有権を持つ構造体を作ることができますが、そのような構造体は所有権チェックを受けらないためメモリ安全が担保されません。そのため解放する関数を構造体に対して定義して解放忘れがないように注意してください。また、ヒープ領域とスタック領域のどちらでも受け入れる場合は型を&mut:Tまたはptr:Tにしてヒープ領域の場合は別で所有者を設けてメモリの管理をすることを推奨します。
struct Parson{
*:char name, // nameが所有権を持つ
i64 age,
}
fn Parson_free(ptr:Parson p){
free(pmove(_>(p,name))); // nameを解放
}
fn Parson_new(*:char name, i64 age):Parson{
#=(parson, _, Parson);
=(.(parson,name),pmove(name)); // 所有権が構造体のフィールドに譲渡される
=(.(parson,age),age);
return parson;
}
fn main():i32{
#=(name,malloc(6,char),*:char);
memcpy(name,"Kazane",6); // ヒープ領域に文字列をコピー
#=(p,Parson_new(pmove(name),19),Parson);
println(format("name: %s\nage %d",.(p,name),.(p,age)));
Parson_free(&_(p)); // 解放
return 0s;
}
複数ファイルで開発
1つのファイルだけでプロジェクトを作ると、プロジェクトの規模が大きくなってくると一つのファイルが巨大になってしまい、コード量が増えて全体を把握しづらくなります。また、ある機能の修正が他の機能に影響してしまったり、複数人で分担しにくいという問題が起きます。そこでWapLではファイルを分割してコンパイル時につなげることができます。これにより、上記の問題を解決できるだけでなく、コードの再利用性も高くなります。また、WapLの提供している標準ライブラリを使うにもこの機能を使います。この章では複数ファイルで開発をする方法と標準ライブラリの使い方を説明します。
ファイルをつなげる
他ファイルの関数や構造体を使う
以下のようなファイル構成のときを例に説明します
src
├── SayHello.wapl
└── main.wapl
ファイル:SayHello.waplにある以下の関数をファイル:main.waplで呼びたいです。
fn Hello(){
println("HELLO!!!!!");
}
こんな時にはuseキーワードを使ってつなげたいファイルのパスをしていします。
ファイル:main.wapl
use "./src/SayHello.wapl"
fn main():i32{
Hello();
return 0s;
}
パスは文字列リテラルと同じでダブルクォーテーションで囲んで書きます。
同様に構造体も持ち込むことができます。
以下のようなファイル構成のときを例に説明します。
src
├── Complex.wapl
└── main.wapl
ファイル:Complex.wapl
struct Complex{
f64 re,
f64 im
}
fn Complex_new(f64 re,f64 im):Complex{
#=(cplx, _, Complex);
=(.(cplx,re), re);
=(.(cplx,im), im);
return cplx;
}
fn Complex_show(Complex c){
println(format("c = %g + %gi",.(c,re),.(c,im)));
}
この複素数の構造体をファイル:main.waplで使います。
use "./src/Complex.wapl"
fn main():i32{
#=(c, Complex_new(1.0,2.0), Complex);
Complex_show(c);
return 0s;
}
このようにして複数ファイルで開発をすることができます。
標準ライブラリを使う
wapl-cli newを使ってプロジェクトを作った場合はすでにstdというディレクトリが作られてその中に標準ライブラリのファイルが入っていると思います。もしそうでなければwapl-cli std_loadでデフォルトに設定されているwaplcのバージョンに合わせた標準ライブラリを取得することもできます。
$ wapl-cli std_laod
標準ライブラリもファイルをつなげるで説明した方法で使うことができます。例えば標準ライブラリにあるtime.waplを使いたいときは以下のようにuse "./std/time.wapl"とすることで使えます。
use "./std/time.wapl"
fn main():i32{
#=(start, Time_now(),timespec);
#=(i,0,i64);
loopif:(<(i,10000000)){
print("WapL");
=(i,+(i,1));
}
println("");
#=(end, Time_now(), timespec);
println(format("%g秒経過",Time_delta(start,end)));
return 0s;
}
また、すべての標準ライブラリを一括でつなげたいときはuse "./std/allstd.wapl"でできます。
標準ライブラリの紹介
| 名前 | 機能 |
|---|---|
| allstd | すべての標準ライブラリを一括でuse |
| assign_and_cal | インクリメント++や+=といった計算と代入を同時に行う関数を提供 |
| HelloWorld | Hello World!と表示するだけの関数がある |
| iterator | イテレータ(0.1.9現在まだ作り途中) |
| longinput | 入力を配列で受け取るための関数がある |
| math | 絶対値を返す関数やmath_PIやmath_Eで数学の定数を返す |
| sort | ソートをする関数のコレクション |
| String | String型(文字列のポインタ,長さ,容量からなる)の構造体とそれに関連する関数がある |
| time | 時間の取得や停止 |
| utility | いろいろ |
| VecT | VecT型(任意型の配列,長さ,容量,型のバイト数からなる)任意型の配列の構造体とそれに関連する関数がある |
ライブラリを作って公開する
https://github.com/kazanefu/WapL_Libraryにライブラリのフォルダを追加してライブラリ名でブランチを切ってpushしてプルリクをください。
その他
- C言語の標準ライブラリとリンク
C言語標準ライブラリとリンク
WapLではC言語の標準ライブラリとリンクして関数や構造体を使うことができます。
関数のリンク
関数をリンクさせるには関数名とシグネチャを一致させてdeclareで宣言するだけでよいです。
declare printf(ptr:char,...):i32
fn main():i32{
printf("Hello, %s\n","WapL");
return 0s;
}
これだけでC言語の標準ライブラリのprintfが使えます。
構造体のリンク
こっちは関数よりももっと簡単で単に同じ名前で中のフィールドの型も一致させるだけでC言語の構造体と同じものとして扱うことができます。これはWapL標準ライブラリのtime.waplでも使っているのでそれを例に見てみましょう。
struct timespec {
i64 tv_sec,
i64 tv_nsec,
}
declare timespec_get(ptr:timespec, i32):i32;
declare nanosleep(ptr:timespec, ptr):i32;
fn Time_get(&mut:timespec ts):i32{
#=(i,1,i64);
return timespec_get(ts, *_(&_(i),i32));
}
fn Time_sleep(&mut:timespec ts):i32{
#=(null,_,ptr);
return nanosleep(ts, null);
}
fn Time_new():timespec{
#=(time,_,timespec);
=(.(time,tv_sec),0);
=(.(time,tv_nsec),0);
return time;
}
fn Time_delta(timespec start,timespec end):f64{
#=(sec_diff, -(.(end,tv_sec), .(start,tv_sec)), i64);
#=(nsec_diff, -(.(end,tv_nsec), .(start,tv_nsec)), i64);
#=(elapsed,
+(as_f64(sec_diff), *(as_f64(nsec_diff), 0.000000001)),
f64
);
return elapsed;
}
fn Time_now():timespec{
#=(time,_,timespec);
=(.(time,tv_sec),0);
=(.(time,tv_nsec),0);
#=(i,1,i64);
timespec_get(&_(time), *_(&_(i),i32));
return time;
}
fn Time_as_f64(timespec ts):f64{
#=(sec_diff, .(ts,tv_sec), i64);
#=(nsec_diff,.(ts,tv_nsec), i64);
#=(f_num,
+(as_f64(sec_diff), *(as_f64(nsec_diff), 0.000000001)),
f64
);
return f_num;
}
ここではC言語のtimespecという構造体を同じ形式でWapLでも定義することでC言語の関数のtimespec_getやnanosleepなどへの引数として渡すことができています。
組み込み関数一覧
| 関数名 | 引数 | 戻り値 | 説明 |
|---|---|---|---|
| &_ | 参照を作れるものなら何でも | ptr:T | 参照を作る |
| *_ | ポインタ,(読み取る型) | そのポインタの指す型または指定した型 | 参照外し |
| sqrt | f64 | f64 | 平方根 |
| cos | f64 | f64 | 余弦 |
| sin | f64 | f64 | 正弦 |
| pow | f64,f64 | f64 | 第一引数の第二引数乗 |
| exp | f64 | f64 | ネイピア数の引数乗 |
付録
WapLコンパイラの作り方
環境:
- WSL2: Ubuntu24.04
- Rust 1.91.1
- Clang/LLVM 21.1.6
- inkwell 0.7.1
Rustをインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
バージョン確認
rustc --version
cargo --version
LLVMのビルドとかで必要なものたちをインストール
sudo apt install -y build-essential cmake ninja-build git python3 libffi-dev
LLVM 21.1.6 のソースコードをダウンロード
cd ~
git clone https://github.com/llvm/llvm-project.git --branch llvmorg-21.1.6 --depth=1
LLVM をビルド
cd ~/llvm-project
mkdir build
cd build
cmake -G Ninja ../llvm \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_PROJECTS="clang;lld;polly" \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DLLVM_ENABLE_RTTI=ON \
-DLLVM_ENABLE_EH=ON \
-DLLVM_ENABLE_TERMINFO=OFF \
-DLLVM_ENABLE_Z3_SOLVER=OFF \
-DLLVM_ENABLE_BINDINGS=OFF \
-DLLVM_ENABLE_LIBXML2=OFF \
-DLLVM_ENABLE_ZSTD=OFF \
-DLLVM_ENABLE_ASSERTIONS=OFF \
-DCMAKE_INSTALL_PREFIX=/usr/local/llvm-21
ninja -j$(nproc)
ビルドには少し時間がかかります
LLVMをインストール
sudo ninja install
パスを通す
echo 'export PATH=/usr/local/llvm-21/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/llvm-21/lib:$LD_LIBRARY_PATH' >> ~/.bashrc
echo 'export LLVM_SYS_210_PREFIX=/usr/local/llvm-21' >> ~/.bashrc
source ~/.bashrc
バージョン確認
Clang --version
llvm-config --version
WapLコンパイラの大まかな設計
1.コードを書いたファイルを文字列として読み込む
↓
2.文字列からトークン列に変換する
↓
3.トークン列から抽象構文木(Abstruct Syntax Tree以後AST)を作る
↓
4.ASTから意味解析や超簡易的な所有権/借用のチェックと同時に中間言語としてLLVM IRを生成
↓
6.LLVM IRをClangに投げて最適化や実行ファイルの作成をしてもらう
一応2~4についてそれぞれ軽く説明します。
2.文字列からトークン列に変換する
fn main():i32{
println("Hello, world!");
return 0s;
}
#![allow(unused)]
fn main() {
0:Fn
1:Ident("main")
2:Lsep(LParen)
3:Rsep(RParen)
4:Colon
5:Ident("i32")
6:Lsep(LBrace)
7:Ident("println")
8:Lsep(LParen)
9:StringLiteral("Hello, world!")
10:Rsep(RParen)
11:Semicolon
12:Return
13:IntshortNumber(0)
14:Semicolon
15:Rsep(RBrace)
}
文字列からトークン列に変換する段階では上のように文字列を意味のある最小単位に分割していきます。 WapLではトークンは大きく識別子,定数値,予約語,区切り記号の4種類があり、一般的な言語にある演算子などはすべて識別子に統合されています。これはほとんどすべて関数呼び出しの記法と同じ記法で書くため演算子も関数名として扱ってしまうことができるからです。
WapLでのトークンの構成
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
//識別子
Ident(String), //変数名や関数名
//定数値
IntNumber(i64), //整数値
FloatNumber(f64), //浮動小数点数
IntshortNumber(i32),// 32bit整数
FloatshortNumber(f32),// 32bit 浮動小数点数
StringLiteral(String), //ダブルクォーテーションで囲んだ文字列
CharLiteral(char), //シングルクォーテーションで囲んだ文字
BoolLiteral(bool), // true と false
// キーワード
ArrayCall, // Array(e1,e2,e3,...)のように配列を書くため他の関数呼び出しとの区別用
Fn, //関数 fn
Struct, //構造体 struct
Point, //ラベル設置 point
Warpto, //ジャンプ warpto
WarptoIf, //条件付きジャンプ warptoif
Return, //戻り値 return
Import, //他ファイルを取り込む use
LoopIf, //名前&条件付き繰り返し loopif
Declare, //関数の宣言のみ declare
//記号や括弧
Comma,// ","
Semicolon, // ";"
Colon, // ":"
Lsep(LSeparator), //括弧の左側
Rsep(RSeparator), //括弧の右側
EOF, // 終わり
}
#[derive(Debug, Clone, PartialEq)]
pub enum LSeparator {
LParen, // "("
LBrace, // "{"
}
#[derive(Debug, Clone, PartialEq)]
pub enum RSeparator {
RParen, // ")"
RBrace, // "}"
}
}
3.トークン列からASTを作る
2.で生成したトークン列は以下のように変換されます
#![allow(unused)]
fn main() {
Function(
Function {
name: "main",
return_type: Ident("i32"),
args: [],
body: [
Stmt {
expr: Call {
name: "_TOPLEVEL_",
args: []
}
},
Stmt {
expr: Call {
name: "println",
args: [
String("Hello, world!")
]
}
},
Stmt {
expr: Return([
IntSNumber(0)
])
}
]
}
)
}
注:WapLでは
main関数の初めに_TOPLEVEL_を呼んで関数の外に書いた処理を先に行うため自動で_TOPLEVEL_が加えられている。
このようにトークン列をASTにすることで上のように構造を持たせてトークンどうしの関係を表現することができます。WapLでのASTの構造は以下のように定義されています。
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub enum Expr {
IntNumber(i64), //i64リテラル
FloatNumber(f64), //f64リテラル
IntSNumber(i32), //i32リテラル
FloatSNumber(f32), //f32リテラル
String(String), //文字列リテラル
Char(char), //charリテラル
Bool(bool), //boolリテラル
ArrayLiteral(Vec<Expr>), //Array(e1,e2,e3,...)という形の配列リテラル
Ident(String), //変数名,型名,ワープに使うラベル名前などStringではない文字列
Return(Vec<Expr>), //関数の返り値
Point(Vec<Expr>), //ワープに使うラベル
Call {
name: String,
args: Vec<Expr>,
}, //関数呼び出し
Warp {
name: String,
args: Vec<Expr>,
}, //warpto,warptoif
StructVal {
_name: String,
_args: Vec<Expr>,
}, //structの実体
TypeApply {
base: String,
args: Vec<Expr>,
}, //ptr:typeとかのbase:type複雑な型
Loopif {
name: String,
cond: Vec<Expr>,
body: Vec<Stmt>,
}, //loopif:name(cond){do}
}
#[derive(Debug, Clone)]
pub struct Stmt {
pub expr: Expr,
}
#[derive(Debug, Clone)]
pub enum TopLevel {
Function(Function),
Struct(Struct),
Declare(Declare),
}
#[derive(Debug, Clone)]
pub struct Declare {
pub name: String,
pub return_type: Expr,
pub args: Vec<Expr>,
pub is_vararg: bool,
}
#[derive(Debug, Clone)]
pub struct Function {
pub name: String,
pub return_type: Expr,
pub args: Vec<(Expr, Expr)>, //(type,name)
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone)]
pub struct Struct {
pub name: String,
pub _return_type: Expr,
pub args: Vec<(Expr, Expr)>, //(type,name)
}
#[derive(Debug, Clone)]
pub struct Program {
pub functions: Vec<TopLevel>, //functions & structs & toplevel call
pub has_main: bool,
}
}
4.中間言語LLVM IRを生成
; ModuleID = 'wapl_module'
source_filename = "wapl_module"
@str_0 = private unnamed_addr constant [14 x i8] c"Hello, world!\00", align 1
@println_fmt_1 = private unnamed_addr constant [4 x i8] c"%s\0A\00", align 1
declare i64 @strtol(ptr, ptr, i32)
declare double @atof(ptr)
declare i32 @printf(ptr, ...)
declare i32 @sprintf(ptr, ptr, ...)
declare ptr @realloc(ptr, i64)
declare ptr @malloc(i64)
declare void @free(ptr)
declare i32 @scanf(ptr, ...)
define i32 @_TOPLEVEL_() {
entry:
ret i32 0
}
define i32 @main() {
entry:
%ret_val = alloca i32, align 4
%calltmp = call i32 @_TOPLEVEL_()
%printf = call i32 (ptr, ...) @printf(ptr @println_fmt_1, ptr @str_0)
ret i32 0
}
この段階では3.で生成したASTから意味を読み取り,それに合わせてLLVM IRを生成します。WapLではデフォルトでラッパーを用意しているCの関数の宣言をや文字列リテラル、_TOPLEVEL_関数などが最初に作られて、その後、実際にWapLで書いたプログラムが書かれます。mainでは、entryブロックを設置、戻り値のためのメモリを一応確保(初期はreturnを書かないでもいいようにする設計だった名残で今はほとんど死んでる)、_TOPLEVEL_を呼ぶ、printlnはC言語のprintfのラッパーなのでprintfを呼ぶ、0を返すということが書かれています。
トークン列の生成
では具体的にどのようにトークン列を生成しているのか見てみましょう。 まずトークンを作る構造体を以下のように定義します。
#![allow(unused)]
fn main() {
pub struct Tokenizer {
chars: Vec<char>,
pos: usize,
}
impl Tokenizer{
pub fn new(input: &str) -> Self {
Self {
chars: input.chars().collect(),
pos: 0,
}
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).cloned()
}
fn next_char(&mut self) -> Option<char> {
let ch = self.peek()?;
self.pos += 1;
Some(ch)
}
fn match_next(&mut self, expected: char) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
}
}
newで初期化、peekで今いる場所の文字を読み取り、next_charで今いる場所の文字を読み取って文字を一つ進める、match_nextで今いる場所の文字が期待するものと一致すれば進める。というように基本的な動きを定義します。
次に、impl Tokenizerに空白とコメントを飛ばすメソッドを追加します。
#![allow(unused)]
fn main() {
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek() {
if c.is_whitespace() {
self.pos += 1;
} else {
self.skip_comment();
break;
}
}
}
fn skip_comment(&mut self) {
loop {
if self.peek() != Some('/') || self.chars.get(self.pos + 1) != Some(&'/') {
return;
}
self.pos += 2;
while let Some(c) = self.peek() {
self.pos += 1;
if c == '\n' {
break;
}
}
self.skip_whitespace();
}
}
}
skip_whitespaceでは文字が空白である限り進め、空白ではなくなったときにコメントがあればそれもスキップするようにしています。skip_commentでは//を見つけてその分の2文字進めて改行が来るまで進め続けて改行が来たら空白をスキップしてまたコメントが来ないか確認するということを繰り返して、コメントじゃないのが来たら終わります。
次に、文字列や文字のリテラルをトークンにするメソッドをimpl Tokenizerに追加します。ファイルや標準入力などから受け取った文字列は"\n"が"\\n"になってたりなどするのでそれを書いた通りの形に戻したり、\x61や\u{A66E}のように文字コードでの入力をできるようにします。
#![allow(unused)]
fn main() {
fn string_and_char_tokenize(&mut self) -> String {
let mut s = String::new();
while let Some(c) = self.next_char() {
if c == '"' || c == '\'' {
break;
}
if c != '\\' {
s.push(c);
continue;
}
if self.match_next('n') {
s.push('\n');
} else if self.match_next('0') {
s.push('\0');
} else if self.match_next('\\') {
s.push('\\');
} else if self.match_next('r') {
s.push('\r');
} else if self.match_next('t') {
s.push('\t');
} else if self.match_next('"') {
s.push('"');
} else if self.match_next('x') {
let h1 = self.next_char().unwrap();
let h2 = self.next_char().unwrap();
let byte = u8::from_str_radix(&format!("{}{}", h1, h2), 16).unwrap();
s.push(byte as char);
} else if self.match_next('u') {
if self.match_next('{') {
let mut hex = String::new();
while let Some(cc) = self.next_char() {
if cc == '}' {
break;
}
hex.push(cc);
}
let cp = u32::from_str_radix(&hex, 16).unwrap();
let cch = char::from_u32(cp).unwrap();
s.push(cch);
} else {
s.push('\\');
s.push('u');
}
} else {
s.push(c);
}
}
s
}
}
見ての通り'\\'が来たらその場所を置き換えるようにしています。
最後にこれらのメソッドを使ってトークンを作ってトークン列に追加していくメソッドを追加します。まず空白とコメントをスキップして、次に文字があるかを確かめ、その後、文字列リテラルか->文字リテラルか->数値リテラルか(もしそうだとしたら整数か浮動小数点数かまた何ビットか)->識別子やキーワード->括弧や記号、といった順で確かめていきトークン列に追加するということをしています。
#![allow(unused)]
fn main() {
pub fn next_token(&mut self) -> Token {
loop {
self.skip_whitespace();
self.skip_comment();
break;
}
let ch = match self.next_char() {
Some(c) => c,
None => return Token::EOF,
};
// ----- String -----
if ch == '"' {
return Token::StringLiteral(self.string_and_char_tokenize());
}
//----char-----
if ch == '\'' {
let c = self.string_and_char_tokenize().chars().collect::<Vec<_>>()[0];
return Token::CharLiteral(c);
}
// ----- Number -----
if ch.is_ascii_digit() || ch == '-' {
let mut s = ch.to_string();
let mut is_float = false;
let mut is_short = false;
while let Some(c) = self.peek() {
if c.is_ascii_digit() || c == '.' ||c=='s'{
if c == '.' {
is_float = true;
}
if c == 's'{
is_short = true;
self.pos += 1;
break;
}
s.push(c);
self.pos += 1;
} else {
break;
}
}
if s != "-" {
if is_float {
return if !is_short{Token::FloatNumber(s.parse::<f64>().unwrap())}else{Token::FloatshortNumber(s.parse::<f32>().unwrap())};
} else {
return if !is_short{Token::IntNumber(s.parse::<i64>().unwrap())}else{Token::IntshortNumber(s.parse::<i32>().unwrap())};
}
}
}
// ----- Identifier / Keyword -----
if !ch.is_ascii_digit()
&& ch != ','
&& ch != ';'
&& ch != '{'
&& ch != '}'
&& ch != '('
&& ch != ')'
&& ch != ':'
{
let mut s = ch.to_string();
while let Some(c) = self.peek() {
if c != ','
&& c != ';'
&& c != '{'
&& c != '}'
&& c != '('
&& c != ')'
&& c != ' '
&& c != ':'
&& c != '\n'
{
s.push(c);
self.pos += 1;
} else {
break;
}
}
return match s.as_str() {
"fn" => Token::Fn,
"struct" => Token::Struct,
"point" => Token::Point,
"warpto" => Token::Warpto,
"warptoif" => Token::WarptoIf,
"true" => Token::BoolLiteral(true),
"false" => Token::BoolLiteral(false),
"return" => Token::Return,
"Array" => Token::ArrayCall,
"import" | "use" => Token::Import,
"loopif" => Token::LoopIf,
"declare" => Token::Declare,
_ => Token::Ident(s),
};
}
// ----- Operators / Signs -----
match ch {
',' => Token::Comma,
';' => Token::Semicolon,
':' => Token::Colon,
'(' => Token::Lsep(LSeparator::LParen),
')' => Token::Rsep(RSeparator::RParen),
'{' => Token::Lsep(LSeparator::LBrace),
'}' => Token::Rsep(RSeparator::RBrace),
_ => Token::EOF,
}
}
pub fn tokenize(&mut self) -> Vec<Token> {
let mut tokens = Vec::new();
loop {
let t = self.next_token();
if t == Token::EOF {
break;
}
tokens.push(t);
}
tokens
}
}
ASTの生成
まずは、ASTを作る構造体を以下のように定義します。
#![allow(unused)]
fn main() {
pub struct Parser {
tokens: Vec<Token>,
pos: usize,
has_main: bool,
}
impl Parser{
pub fn new(tokens: Vec<Token>) -> Self {
Self {
tokens,
pos: 0,
has_main: false,
}
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos)
}
fn peek_back(&self) -> Option<&Token> {
self.tokens.get(self.pos - 1)
}
fn _peek_front(&self) -> Option<&Token> {
self.tokens.get(self.pos + 1)
}
fn no_return_next(&mut self) {
self.pos += 1;
}
fn no_return_back(&mut self) {
self.pos -= 1;
}
fn next(&mut self) -> Option<&Token> {
let t = self.tokens.get(self.pos);
self.pos += 1;
t
}
fn expect(&mut self, expected: &Token) {
let t = self.next().expect("unexpected EOF");
if t != expected {
panic!("expected {:?}, got {:?}", expected, t);
}
}
fn check(&self, expected: &Token) -> bool {
let t = self.peek().expect("unexpected EOF");
t == expected
}
fn consume_comma(&mut self) {
match self.peek() {
Some(Token::Comma) => {
self.next();
}
_ => {}
}
}
fn consume_semicolon(&mut self) {
match self.peek() {
Some(Token::Semicolon) => {
self.next();
}
_ => {}
}
}
}
}
Tokenizerと同様にnewで初期化、peekで今いるとこのトークンを読み取る、peek_backで一つ前を読み取る、_peek_frontは使ってないが一つ先を読み取るもの、no_return_backとno_return_nextは戻り値なしで次に進めたり一つ戻したりする、nextで今のを読み取って次に進める、expectは期待するトークンと異なればエラーを出す、checkは期待するトークンと一致するかのboolを返す、consume_XXXはXXXを飛ばす。という基本的な動作を定義する。
次にuseで他のファイルもトークン列を作る->ASTを再帰的に作っていくためのメソッドを追加する。ここではすでにASTを作ったファイルを重複してuseで呼びだしてしまわないようにASTを作ったらファイル名を記録していき、その記録にまだ登録されてないことを確認してからASTをつくっている。
#![allow(unused)]
fn main() {
fn connect_use(&mut self, import_map: &mut Vec<String>, funcs: &mut Vec<TopLevel>) {
self.no_return_next();
let Token::StringLiteral(path) = self.next().unwrap_or(&Token::EOF) else {
return;
};
let source = fs::read_to_string(path).expect("ファイルを読み込めませんでした");
if import_map.contains(&path) {
return;
}
let mut tokenizer = Tokenizer::new(source.as_str());
let tokens = tokenizer.tokenize();
let mut parser = Parser::new(tokens);
let parsed = parser.parse_program(import_map);
for module_ast in parsed.functions {
funcs.push(module_ast);
}
import_map.push(path.to_string());
}
}
ではここからパースしてASTを作るところを見てみよう。プログラム全体のパースを駆動するのは以下のメソッドを使う。
#![allow(unused)]
fn main() {
pub fn parse_program(&mut self, import_map: &mut Vec<String>) -> Program {
let mut funcs = Vec::new();
while let Some(tok) = self.peek() {
match tok {
Token::Import => {
self.connect_use(import_map, &mut funcs);
}
Token::Fn => {
funcs.push(TopLevel::Function(self.parse_function()));
}
Token::Struct => {
funcs.push(TopLevel::Struct(self.parse_struct()));
}
Token::Declare => {
funcs.push(TopLevel::Declare(self.parse_declare()));
}
Token::Semicolon => {
self.no_return_next();
}
_ => {
// toplevel eval
let expr = self.parse_expr();
match expr {
Expr::Call { name, args: _ } if name == "main" => {}
other => {
funcs.push(TopLevel::Function(Function {
name: "toplevel_child".to_string(),
return_type: Expr::Ident("void".to_string()),
args: vec![],
body: vec![Stmt { expr:other }],
}));
}
}
}
}
}
Program {
functions: funcs,
has_main: self.has_main,
}
}
}
また、それぞれの文と式を以下のようなメソッドたちを使ってパースする。
#![allow(unused)]
fn main() {
// -------------------------
// Parse function
// fn name():i32 { ... }
// -------------------------
fn parse_function(&mut self) -> Function {
self.expect(&Token::Fn);
let mut is_main = false;
// function name
let mut name = match self.next() {
Some(Token::Ident(s)) => s.clone(),
other => panic!("expected function name, got {:?}", other),
};
name = if name == "main" {
is_main = true;
self.has_main = true;
"main".to_string()
} else {
name
};
// (type name,type name,...)
self.expect(&Token::Lsep(LSeparator::LParen));
let mut args = Vec::new();
while let Some(token_arg_type) = self.peek() {
if *token_arg_type == Token::Rsep(RSeparator::RParen) {
break;
}
self.no_return_next();
let arg_type = match self.peek_back() {
Some(Token::Ident(s)) => self.parse_type_apply(s.clone()),
other => panic!("expected arg type, got {:?}", other),
};
let arg_name = match self.next() {
Some(Token::Ident(s)) => Expr::Ident(s.clone()),
other => panic!("expected arg name, got {:?}", other),
};
args.push((arg_type, arg_name));
self.consume_comma();
}
self.expect(&Token::Rsep(RSeparator::RParen));
let mut return_type = Expr::Ident("void".to_string());
if self.check(&Token::Colon) {
self.expect(&Token::Colon);
if !self.check(&Token::Lsep(LSeparator::LBrace)) {
return_type = self.parse_return_type();
}
}
// { statements }
self.expect(&Token::Lsep(LSeparator::LBrace));
self.consume_semicolon();
let mut stmts = Vec::new();
if is_main {
stmts.push(Stmt {
expr: Expr::Call {
name: "_TOPLEVEL_".to_string(),
args: vec![],
},
})
}
while let Some(tok) = self.peek() {
if *tok == Token::Rsep(RSeparator::RBrace) {
break;
}
// optional ;
if let Some(Token::Semicolon) = self.peek() {
self.next();
}
let expr = self.parse_expr();
stmts.push(Stmt { expr });
// optional ;
if let Some(Token::Semicolon) = self.peek() {
self.next();
}
}
self.expect(&Token::Rsep(RSeparator::RBrace));
self.consume_semicolon();
Function {
name,
return_type,
args: args,
body: stmts,
}
}
// -------------------------
// Parse struct
// struct name{ i32 a, ptr b,..., }
//
fn parse_struct(&mut self) -> Struct {
self.expect(&Token::Struct);
// struct name
let (name, return_type) = match self.next() {
Some(Token::Ident(s)) => (s.clone(), Expr::Ident(s.clone())),
other => panic!("expected struct name, got {:?}", other),
};
self.expect(&Token::Lsep(LSeparator::LBrace));
self.consume_semicolon();
let mut args = Vec::new();
while let Some(token_arg_type) = self.peek() {
if *token_arg_type == Token::Rsep(RSeparator::RBrace) {
break;
}
self.no_return_next();
let arg_type = match self.peek_back() {
Some(Token::Ident(s)) => self.parse_type_apply(s.clone()), //Expr::Ident(s.clone()),
other => panic!("expected arg type, got {:?}", other),
};
let arg_name = match self.next() {
Some(Token::Ident(s)) => Expr::Ident(s.clone()),
other => panic!("expected arg name, got {:?}", other),
};
args.push((arg_type, arg_name));
self.consume_comma();
}
self.expect(&Token::Rsep(RSeparator::RBrace));
Struct {
name,
_return_type: return_type,
args,
}
}
fn parse_declare(&mut self) -> Declare {
self.expect(&Token::Declare);
// function name
let mut name = match self.next() {
Some(Token::Ident(s)) => s.clone(),
other => panic!("expected function name, got {:?}", other),
};
name = if name == "main" {
"main".to_string()
} else {
name
};
// (type name,type name,...)
self.expect(&Token::Lsep(LSeparator::LParen));
let mut args = Vec::new();
let mut is_vararg = false;
while let Some(token_arg_type) = self.peek() {
if *token_arg_type == Token::Rsep(RSeparator::RParen) {
break;
}
self.no_return_next();
let arg_type = match self.peek_back() {
Some(Token::Ident(s)) => self.parse_type_apply(s.clone()),
other => panic!("expected arg type, got {:?}", other),
};
match arg_type {
Expr::Ident(s) if s == "..." => {
is_vararg = true;
}
_ => {
args.push(arg_type);
}
}
self.consume_comma();
}
self.expect(&Token::Rsep(RSeparator::RParen));
let mut return_type = Expr::Ident("void".to_string());
if self.check(&Token::Colon) {
self.expect(&Token::Colon);
if !self.check(&Token::Lsep(LSeparator::LBrace)) {
return_type = self.parse_return_type();
}
}
self.consume_semicolon();
self.consume_semicolon();
Declare {
name,
return_type,
args: args,
is_vararg,
}
}
// -------------------------
// Parse expression (function-call style)
//
// +(1,2)
// =(a, 1, i32)
// println(a)
// warpto(label)
// loopif:name(cond){}
// -------------------------
fn parse_return_type(&mut self) -> Expr {
self.no_return_next();
let token = self.peek_back();
match token {
Some(Token::Ident(name)) => {
let front = self.peek();
match front {
Some(Token::Colon) => self.parse_type_apply(name.clone()),
Some(_) => Expr::Ident(name.clone()),
_ => panic!("unexpected EOF in expression"),
}
}
Some(tok) => panic!(
"{}expression begins with unexpected token {:?}",
self.pos, tok
),
None => panic!("unexpected EOF in expression"),
}
}
fn parse_expr(&mut self) -> Expr {
self.no_return_next();
let token = self.peek_back();
match token {
Some(Token::IntNumber(n)) => Expr::IntNumber(*n),
Some(Token::FloatNumber(n)) => Expr::FloatNumber(*n),
Some(Token::IntshortNumber(n)) => Expr::IntSNumber(*n),
Some(Token::FloatshortNumber(n)) => Expr::FloatSNumber(*n),
Some(Token::StringLiteral(s)) => Expr::String(s.clone()),
Some(Token::CharLiteral(c)) => Expr::Char(*c),
Some(Token::BoolLiteral(b)) => Expr::Bool(*b),
Some(Token::Return) => Expr::Return(vec![self.parse_expr()]),
Some(Token::Point) => Expr::Point(vec![self.parse_expr()]),
Some(Token::Warpto) => self.parse_call("warpto".to_string()),
Some(Token::WarptoIf) => self.parse_call("warptoif".to_string()),
Some(Token::LoopIf) => self.parse_loopif(),
Some(Token::ArrayCall) => self.parse_call("Array".to_string()),
Some(Token::Ident(name)) => self.parse_ident_expr(name.clone()),
Some(tok) => panic!(
"{}expression begins with unexpected token {:?}",
self.pos, tok
),
None => panic!("unexpected EOF in expression"),
}
}
fn parse_ident_expr(&mut self, name: String) -> Expr {
let front = self.peek();
match front {
Some(Token::Lsep(LSeparator::LParen)) => self.parse_call(name.clone()),
Some(Token::Lsep(LSeparator::LBrace)) => self.parse_structval(name.clone()),
Some(Token::Colon) => self.parse_type_apply(name.clone()),
Some(_) => Expr::Ident(name.clone()),
_ => panic!("unexpected EOF in expression"),
}
}
// -------------------------
// Function call parsing
//
// name(arg1, arg2, ...)
// -------------------------
fn parse_type_apply(&mut self, name: String) -> Expr {
match self.next() {
Some(Token::Colon) => {}
_ => {
self.no_return_back();
return Expr::Ident(name.clone());
} //normal types
}
let mut args = Vec::new();
match self.peek() {
_ => {
let arg = self.parse_return_type();
args.push(arg);
self.consume_comma();
}
}
Expr::TypeApply { base: name, args }
}
fn parse_structval(&mut self, name: String) -> Expr {
match self.next() {
Some(Token::Lsep(LSeparator::LBrace)) => {}
other => panic!(
"expected '{{' after function name {:?}, got {:?}",
name, other
),
}
let mut args = Vec::new();
loop {
match self.peek() {
Some(Token::Rsep(RSeparator::RBrace)) => {
self.next();
break;
}
_ => {
let arg = self.parse_expr();
args.push(arg);
self.consume_comma();
}
}
}
Expr::StructVal {
_name: name,
_args: args,
}
}
fn parse_call(&mut self, mut name: String) -> Expr {
match self.next() {
Some(Token::Lsep(LSeparator::LParen)) => {}
other => panic!(
"expected '(' after function name {:?}, got {:?}",
name, other
),
}
let mut args = Vec::new();
loop {
match self.peek() {
Some(Token::Rsep(RSeparator::RParen)) => {
self.next();
break;
}
_ => {
let arg = self.parse_expr();
args.push(arg);
self.consume_comma();
}
}
}
if name == "warpto" || name == "warptoif" {
self.consume_semicolon();
return Expr::Warp { name, args };
} else if name == "Array" {
self.consume_semicolon();
return Expr::ArrayLiteral(args);
}
name = if name == "main" {
"main".to_string()
} else {
name
};
self.consume_semicolon();
Expr::Call { name, args }
}
fn parse_loopif(&mut self) -> Expr {
self.expect(&Token::Colon);
// loopif name
let name = match self.next() {
Some(Token::Ident(s)) => s.clone(),
_ => {
self.no_return_back();
"loop".to_string()
}
};
// (cond)
match self.next() {
Some(Token::Lsep(LSeparator::LParen)) => {}
other => panic!("expected '(' after loopif name {:?}, got {:?}", name, other),
}
let mut args = Vec::new();
loop {
match self.peek() {
Some(Token::Rsep(RSeparator::RParen)) => {
self.next();
break;
}
_ => {
let arg = self.parse_expr();
args.push(arg);
self.consume_comma();
}
}
}
// { statements }
self.expect(&Token::Lsep(LSeparator::LBrace));
self.consume_semicolon();
let mut stmts = Vec::new();
while let Some(tok) = self.peek() {
if *tok == Token::Rsep(RSeparator::RBrace) {
break;
}
// optional ;
if let Some(Token::Semicolon) = self.peek() {
self.next();
}
let expr = self.parse_expr();
stmts.push(Stmt { expr });
// optional ;
if let Some(Token::Semicolon) = self.peek() {
self.next();
}
}
self.expect(&Token::Rsep(RSeparator::RBrace));
self.consume_semicolon();
Expr::Loopif {
name,
cond: args,
body: stmts,
}
}
}
見ての通りWapLコンパイラのパーサは再帰下降パーサになっている。
再帰下降構文解析(さいきかこうこうぶんかいせき、英語: Recursive Descent Parsing)は、相互再帰型の手続き(あるいは再帰的でない同等の手続き)で構成されるLL法のトップダウン構文解析であり、各プロシージャが文法の各生成規則を実装することが多い。従って、生成されるプログラムの構造はほぼ正確にその文法を反映したものとなる。そのような実装の構文解析器を再帰下降パーサ(Recursive Descent Parser)と呼ぶ。 wikiより
LLVM IRの生成
以下のような構造体を作る。
#![allow(unused)]
fn main() {
pub struct Codegen<'ctx> {
pub context: &'ctx Context,
pub module: Module<'ctx>,
pub builder: Builder<'ctx>,
pub struct_types: HashMap<String, StructType<'ctx>>,// structの型を名前をキーにして記録
pub struct_fields: HashMap<String, Vec<(String, BasicTypeEnum<'ctx>, u32, Expr)>>, // structのフィールドの名前とインデックスと型をstructの名前をキーにして記録
str_counter: usize, // グローバルに確保した文字列の数
current_fn: Option<FunctionContext<'ctx>>, // 現在コンパイルしている関数とその関数内のpointで作ったラベルやwarptoなどの元ブロック
pub function_types: HashMap<String, Expr>, // 関数の戻り値の型
pub current_owners: HashMap<String, bool>, // 現在の関数全体での所有権を持っているポインタが有効かどうかを記録
pub scope_owners: ScopeOwner, // 現在のスコープで所有権を持ってるポインタが有効かどうかを記録し,リターン時にこれに基づいてメモリリークを検出
}
struct PendingJump<'ctx> {
from: BasicBlock<'ctx>,
}
#[derive(Clone)]
struct VariablesPointerAndTypes<'ctx> {
ptr: PointerValue<'ctx>,
typeexpr: Expr,
}
struct FunctionContext<'ctx> {
function: FunctionValue<'ctx>,
labels: HashMap<String, BasicBlock<'ctx>>, //labels(already exist)
unresolved: HashMap<String, Vec<PendingJump<'ctx>>>, //labels(unresolved)
}
#[derive(Clone)]
pub struct ScopeOwner {
pos: usize,
owners: Vec<HashMap<String, bool>>,
}
}
全体のコンパイルを駆動はメソッドがしています
#![allow(unused)]
fn main() {
pub fn compile_program(&mut self, program: Program) {
if program.has_main {
self.compile_declare(Declare {
name: "_TOPLEVEL_".to_string(),
return_type: Expr::Ident("i32".to_string()),
args: vec![],
is_vararg: false,
});
}
for func in program.functions {
match func {
TopLevel::Function(f) => {
self.compile_function(f);
}
TopLevel::Struct(s) => {
self.compile_struct(s);
}
TopLevel::Declare(d) => {
self.compile_declare(d);
}
}
}
combine_toplevel(&self.module, &self.builder, program.has_main);
}
}
まず最初に_TOPLEVEL_の宣言をしてその後,関数か構造体か関数の宣言かのコンパイルを順に呼び出していきます。そして最後にトップレベルにある式を_TOPLEVEL_にまとめています。
では次は関数のIRを作るところを見てみましょう
#![allow(unused)]
fn main() {
fn compile_function(&mut self, func: Function) {
if self.function_types.contains_key(&func.name) {
panic!("function '{}' already defined", func.name);
}
// --- type of return value ---
let return_type_is_void = matches!(func.return_type, Expr::Ident(ref s) if s == "void");
let return_type_enum = if return_type_is_void {
None
} else {
Some(self.llvm_type_from_expr(&func.return_type))
};
// --- type of arguments ---
let arg_types: Vec<BasicTypeEnum> = func
.args
.iter()
.map(|(ty, _)| self.llvm_type_from_expr(ty))
.collect();
// ---convert to Metadata type ---
let arg_types_meta: Vec<BasicMetadataTypeEnum> =
arg_types.iter().map(|t| (*t).into()).collect();
// --- LLVM gen function ---
let fn_type = if return_type_is_void {
self.context.void_type().fn_type(&arg_types_meta, false)
} else {
return_type_enum.unwrap().fn_type(&arg_types_meta, false)
};
// --- add function ---
let llvm_func = self.module.add_function(&func.name, fn_type, None);
self.function_types
.insert(func.name.clone(), func.return_type);
let entry = self.context.append_basic_block(llvm_func, "entry");
self.builder.position_at_end(entry);
// --- alloca & initialize args ---
self.current_owners = HashMap::new();
self.scope_owners = ScopeOwner::new();
let mut variables: HashMap<String, VariablesPointerAndTypes<'ctx>> = HashMap::new();
for (i, (ty, arg_expr)) in func.args.iter().enumerate() {
let param = llvm_func.get_nth_param(i as u32).unwrap();
// get arg names (Expr::Ident(name))
let arg_name = match arg_expr {
Expr::Ident(name) => name.as_str(),
_ => panic!("Function argument name must be identifier"),
};
param.set_name(arg_name);
// alloca anyway
let alloca = self
.builder
.build_alloca(param.get_type(), arg_name)
.expect("alloca failed");
self.builder.build_store(alloca, param).unwrap();
variables.insert(
arg_name.to_string(),
VariablesPointerAndTypes {
ptr: alloca,
typeexpr: ty.clone(),
},
);
match ty {
Expr::TypeApply { base, args: _args } if base == "*" => {
self.current_owners.insert(arg_name.to_string(), true);
self.scope_owners.set_true(arg_name.to_string());
}
_ => {}
}
}
//alloca return value
let _ret_alloca = if !return_type_is_void {
Some(
self.builder
.build_alloca(return_type_enum.unwrap(), "ret_val"),
)
} else {
None
};
self.current_fn = Some(FunctionContext {
function: llvm_func,
labels: HashMap::new(),
unresolved: HashMap::new(),
});
// --- function body ---
for stmt in func.body {
let _value = self.compile_stmt(&stmt, &mut variables);
}
// --- temporary return ---
if return_type_is_void {
self.builder.build_return(None).unwrap();
} else {
// // 仮に i32 を戻り値として返す
// let zero = self.context.i32_type().const_int(0, false);
// self.builder.build_return(Some(&zero)).unwrap();
}
//Exit from the current function
self.current_fn = None;
}
}
まず、関数がすでに定義されていないかの確認をします。次にASTの戻り値の型をLLVM IRの型に変換します。また、同様にして引数の型もIRの型にします。そしたら、関数の宣言をして記録します。次にスコープと所有権を持つポインタを管理するものを作って、引数をそのスコープ内の変数として読み取れるようにしていきます。その後、関数の本体のコンパイルを文ごとに連続して呼びます。
ではそれぞれの文をコンパイルするメソッドを見ていきましょう。ここでは主に制御フローと戻り値のIRを生成します。まず式が来た場合は式のIRを作るためのメソッドを呼んで任せます。次に、point ラベル名 warpto(行先) warptoif(条件,真のときの行先,偽のときの行先) return 戻り値 loopif:ループ名(条件){...}のそれぞれを分岐してIRにしていっています。Returnのところに注目するとここで所有権を持っているポインタが所有しているメモリが解放されているかを確認してメモリリークを防いでいます。
#![allow(unused)]
fn main() {
fn compile_stmt(
&mut self,
stmt: &Stmt,
variables: &mut HashMap<String, VariablesPointerAndTypes<'ctx>>,
) {
match &stmt.expr {
Expr::IntNumber(_) | Expr::FloatNumber(_) | Expr::Call { .. } | Expr::Ident(_) => {
// compile_expr
self.compile_expr(&stmt.expr, variables);
}
Expr::Point(labels) => {
let label_name = match &labels[0] {
Expr::Ident(s) => Some(s.as_str()),
_ => None,
};
self.gen_point(label_name.expect("point: missing label literal"));
}
Expr::Warp { name, args } => match name.as_str() {
"warpto" => {
//get label name (point NAME <- this)
let label_name = match &args[0] {
Expr::Ident(s) => Some(s.as_str()),
_ => None,
};
self.gen_warpto(label_name.expect("point: missing label literal"));
}
"warptoif" => {
// compile condition value
let cond: BasicValueEnum = self.compile_expr(&args[0], variables).unwrap().0;
let cond_i1 = match cond {
BasicValueEnum::IntValue(v) if v.get_type().get_bit_width() == 1 => v,
_ => panic!("warptoif condition requires boolean (i1) values"),
};
//get label name (point NAME <- this)
// label_name1 = destination if condition true
// label_name2 = destination if condition false (optional)
let label_name1 = match &args[1] {
Expr::Ident(s) => Some(s.as_str()),
_ => None,
};
let label_name2 = match &args.get(2) {
Some(Expr::Ident(s)) => Some(s.as_str()),
_ => None,
};
self.gen_warptoif(
cond_i1,
label_name1.expect("point: missing label literal"),
label_name2,
);
}
_ => {
panic!("warp:not (warpto or warptoif)");
}
},
Expr::Return(vals) => {
if vals.len() != 1 {
panic!("Return must have exactly one value");
}
//compile return value
let ret_val = self
.compile_expr(vals.into_iter().next().unwrap(), variables)
.unwrap()
.0; // unwrap is safe because we already checked vals.len() == 1
self.builder.build_return(Some(&ret_val)).unwrap();
// check memory leak
for i in self.scope_owners.show_current() {
// if there are Not released memory , print error message
if let Some(b) = self.current_owners.get(&i.0)
&& *b
{
println!(
"{}:you need to free or drop pointer {}!",
"Error".red().bold(),
i.0
);
}
}
self.scope_owners.reset_current();
}
Expr::Loopif { name, cond, body } => {
if cond.len() != 1 {
panic!("Loopif:{} conditions must have exactly one value", name);
}
self.compile_loopif(name, &cond[0], body, variables);
}
_ => unimplemented!(),
}
}
}
次は式をIRに落とし込むところを見てみましょう。以下のメソッドではリテラル、変数、コンパイラ組み込みの関数の呼び出し、ユーザー定義の関数の呼び出しをIRにしています。すべてのコンパイラ組み込みの関数について書いてあったりするため非常に長くなっていますので全体の流れだけつかめればよいでしょう。
#![allow(unused)]
fn main() {
fn compile_expr(
&mut self,
expr: &Expr,
variables: &mut HashMap<String, VariablesPointerAndTypes<'ctx>>,
) -> Option<(
BasicValueEnum<'ctx>,
Expr,
Option<VariablesPointerAndTypes<'ctx>>,
)> {
match expr {
// Literals
Expr::IntNumber(n) => Some((
self.context.i64_type().const_int(*n as u64, false).into(),
Expr::Ident("i64".to_string()),
None,
)),
Expr::FloatNumber(n) => Some((
self.context.f64_type().const_float(*n).into(),
Expr::Ident("f64".to_string()),
None,
)),
Expr::IntSNumber(n) => Some((
self.context.i32_type().const_int(*n as u64, false).into(),
Expr::Ident("i32".to_string()),
None,
)),
Expr::FloatSNumber(n) => Some((
self.context.f32_type().const_float((*n).into()).into(),
Expr::Ident("f32".to_string()),
None,
)),
Expr::Bool(b) => Some((
self.context.bool_type().const_int(*b as u64, false).into(),
Expr::Ident("bool".to_string()),
None,
)),
Expr::Char(c) => Some((
self.context.i8_type().const_int(*c as u64, false).into(),
Expr::Ident("char".to_string()),
None,
)),
Expr::String(s) => {
//create unique named global string
let global_str = self
.builder
.build_global_string_ptr(s, &format!("str_{}", self.str_counter))
.unwrap();
self.str_counter += 1;
Some((
global_str.as_pointer_value().into(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![Expr::Ident("char".to_string())],
},
None,
))
}
// Variables
Expr::Ident(name) => {
//get pointer and variable type
let alloca = variables
.get(name)
.expect(&format!("Undefined variable {}", name)); // safe because variable must exist
match &alloca.typeexpr {
//borrow check
Expr::TypeApply { base, args } if base == "*" => {
// check ownership: if pointer has been moved, reading it is prohibited
if !self.current_owners.get(name).expect(&format!(
"{} type is *:{:?} but failed to find in ownerships ",
name, args[0]
)) {
println!(
"{}:\"{}\" already moved. it is prohibited to read moved pointer",
"Error".red().bold(),
name
)
}
}
_ => {}
}
Some((
self.builder
.build_load(self.llvm_type_from_expr(&alloca.typeexpr), alloca.ptr, name)
.unwrap()
.into(),
alloca.typeexpr.clone(),
Some(alloca.clone()),
))
}
Expr::Call { name, args } => match name.as_str() {
//return reference
"ptr" | "&_" => {
//if arg is variable , name is variable name
//else get pointer by from compile_expr
let name = match &args[0] {
Expr::Ident(s) => s,
_ => {
//unwrap because "ptr" or "&_" require an expression with a pointer
let (_exp, ty, p) = self.compile_expr(&args[0], variables).unwrap();
let ptr = p
.expect(&format!(
"ptr() and &_ require an expression with a pointer"
))
.ptr
.as_basic_value_enum();
return Some((
ptr.clone(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![ty],
},
None,
));
}
};
let alloca = get_var_alloca(variables, name);
Some((
alloca.as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![
variables
.get(name)
.expect(&format!("Undefined variable {}", name))
.typeexpr
.clone(),
],
},
None,
))
}
//dereference
//val(pointer , option(what type to load it as))
"val" | "*_" => {
//p = (pointer value,type,...)
// safe: "val" / "*_" always expects an expression that evaluates to a pointer
let p = self.compile_expr(&args[0], variables).unwrap();
let ptr = p.0.into_pointer_value();
let mut load_type = &expr_deref(&p.1); // what type the pointer point
let ty = args.get(1);
// Determine the type to load: default is pointer's base type, override if second argument is provided
load_type = match ty {
Some(t) => t,
None => load_type,
};
let loaded = self
.builder
.build_load(self.llvm_type_from_expr(load_type), ptr, "deref")
.unwrap();
Some((loaded.as_basic_value_enum(), load_type.clone(), None))
}
//declaring and initializing variables
"let" | "#=" => {
// args: [var_name, initial_value, type_name]
let var_name = match &args[0] {
Expr::Ident(s) => s,
_ => panic!("let: first arg must be variable name"),
};
let llvm_type: BasicTypeEnum = self.llvm_type_from_expr(&args[2]);
// If there is an initial value
let init_val_exist = match &args[1] {
Expr::Ident(s) => {
if *s == "_".to_string() {
false
} else {
true
}
}
_ => true,
};
// Check if an initial value is provided; "_" means no initial value
// If no initial value, zero-initialize (works for numeric types and structs)
let init_val = if init_val_exist {
self.compile_expr(&args[1], variables)
} else {
// For structs, initialize with zeroed
Some((
llvm_type.const_zero(),
Expr::Ident("void".to_string()),
None,
))
};
// alloca
let alloca = self
.builder
.build_alloca(llvm_type, var_name)
.expect("fail alloca");
self.builder
.build_store(alloca, init_val.clone().unwrap().0) // safe unwrap: the existence of init_val is already checked
.unwrap();
variables.insert(
var_name.clone(),
VariablesPointerAndTypes {
ptr: alloca,
typeexpr: args[2].clone(),
},
);
// if its type is pointer with Ownership, recoad ownership scope and the entire function ownership
match &args[2] {
Expr::TypeApply { base, args } if base == "*" => {
self.current_owners.insert(var_name.clone(), true);
self.scope_owners.set_true(var_name.clone());
if !init_val_exist {
println!(
"{}: {var_name} is Owner (*:{:?}). it must have value",
"Error".red().bold(),
args
)
}
}
Expr::TypeApply { base:_, args:_ } =>{}
_ => if init_val_exist&&!type_match(&args[2], &init_val.clone().unwrap().1) {
println!("{}: {var_name} Type miss match : expected {:?} found {:?}","Error".red().bold(),&args[2],&init_val.clone().unwrap().1)
},
}
Some((init_val.unwrap().0, Expr::Ident("void".to_string()), None))
}
// Assignment
"=" => match &args[1] {
// Array assign is special
Expr::ArrayLiteral(elems) => Some((
self.codegen_array_assign(&args[0], elems, variables)
.unwrap(), // safe unwrap: codegen_array_assign returns Some for valid array literals
Expr::Ident("void".to_string()),
None, // Array assignment does not return a value because the pointer already exists
)),
_ => {
let value = self.compile_expr(&args[1], variables).unwrap();
let alloca = self.get_pointer_expr(&args[0], variables);
// reassign *_(immutable borrow) or val(immutable borrow) is prohibited
// Check for immutable borrow: cannot reassign *_(immutable) or val(immutable)
if let Expr::Call { name: _name, args } = &args[0]
&& let Some(Expr::Ident(s)) = args.get(0)
&& let Some(val) = variables.get(s)
&& let Expr::TypeApply { base, args: _args } = &val.typeexpr
&& base == "&"
{
println!(
"{} :{} :{:?} is immutable borrow! if you want to reassign, use &mut:T",
"Error".red().bold(),
s,
&val.typeexpr
);
}
self.builder.build_store(alloca, value.0).unwrap();
Some(value)
}
},
// add,sub,mul,div,rem
"+" | "-" | "*" | "/" | "%" => {
let lhs_val = self.compile_expr(&args[0], variables)?;
let rhs_val = self.compile_expr(&args[1], variables)?;
// ===== Type matching: Match the right side to the type of the left side =====
let rhs_casted = match (lhs_val.0, rhs_val.0) {
// ------- When both the left and right are integers -------
(BasicValueEnum::IntValue(l), BasicValueEnum::IntValue(r)) => {
let lhs_bits = l.get_type().get_bit_width();
let rhs_bits = r.get_type().get_bit_width();
let r2 = if lhs_bits > rhs_bits {
// i32 → i64
self.builder
.build_int_s_extend(r, l.get_type(), "int_ext")
.unwrap()
} else if lhs_bits < rhs_bits {
// i64 → i32
self.builder
.build_int_cast(r, l.get_type(), "int_trunc")
.unwrap()
} else {
r
};
BasicValueEnum::IntValue(r2)
}
// ------- When both the left and right are floating point -------
(BasicValueEnum::FloatValue(l), BasicValueEnum::FloatValue(r)) => {
let lhs_ty = l.get_type();
let rhs_ty = r.get_type();
let r2 = if lhs_ty != rhs_ty {
let lhs_bits = float_bit_width(lhs_ty);
let rhs_bits = float_bit_width(rhs_ty);
if lhs_bits > rhs_bits {
// f32 → f64
self.builder.build_float_ext(r, lhs_ty, "fext").unwrap()
} else {
// f64 → f32
self.builder.build_float_trunc(r, lhs_ty, "ftrunc").unwrap()
}
} else {
r
};
BasicValueEnum::FloatValue(r2)
}
// ------- int + float → float (left is float)-------
(BasicValueEnum::FloatValue(l), BasicValueEnum::IntValue(r)) => {
let r2 = self
.builder
.build_signed_int_to_float(r, l.get_type(), "i2f")
.unwrap();
BasicValueEnum::FloatValue(r2)
}
// ------- int + float → int (left is int)-------
(BasicValueEnum::IntValue(l), BasicValueEnum::FloatValue(r)) => {
let r2 = self
.builder
.build_float_to_signed_int(r, l.get_type(), "f2i")
.unwrap();
BasicValueEnum::IntValue(r2)
}
_ => panic!("Unsupported combination in binary operation"),
};
// ===== Calculation from here =====
// Perform the arithmetic operation: operands are now type-matched (int or float)
let result = match (lhs_val.0, rhs_casted) {
(BasicValueEnum::IntValue(l), BasicValueEnum::IntValue(r)) => {
let v = match name.as_str() {
"+" => self.builder.build_int_add(l, r, "add").unwrap(),
"-" => self.builder.build_int_sub(l, r, "sub").unwrap(),
"*" => self.builder.build_int_mul(l, r, "mul").unwrap(),
"/" => self.builder.build_int_signed_div(l, r, "div").unwrap(),
"%" => self.builder.build_int_signed_rem(l, r, "rem").unwrap(),
_ => unreachable!(),
};
v.as_basic_value_enum()
}
(BasicValueEnum::FloatValue(l), BasicValueEnum::FloatValue(r)) => {
let v = match name.as_str() {
"+" => self.builder.build_float_add(l, r, "fadd").unwrap(),
"-" => self.builder.build_float_sub(l, r, "fsub").unwrap(),
"*" => self.builder.build_float_mul(l, r, "fmul").unwrap(),
"/" => self.builder.build_float_div(l, r, "fdiv").unwrap(),
"%" => self.builder.build_float_rem(l, r, "frem").unwrap(),
_ => unreachable!(),
};
v.as_basic_value_enum()
}
_ => unreachable!(),
};
Some((result, lhs_val.1, None))
}
// float Special Functions
"sqrt" | "cos" | "sin" | "pow" | "exp" | "log" => {
let compiled_args: Vec<BasicValueEnum> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap().0;
val
})
.collect();
// check first argument
let val_l = match compiled_args[0] {
BasicValueEnum::FloatValue(v) => v,
// unwrap is safe here because only float arguments are allowed for these intrinsics
_ => panic!("{} require f values", name.as_str()),
};
// If the function takes a second argument (e.g., pow), ensure it is a float
let val_r = if compiled_args.len() > 1 {
match compiled_args[1] {
BasicValueEnum::FloatValue(v) => Some(v),
// unwrap is safe here because only float arguments are allowed for these intrinsics
_ => panic!("at {} expected float", name.as_str()),
}
} else {
None
};
match name.as_str() {
"sqrt" => Some((
self.call_intrinsic("llvm.sqrt.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
"cos" => Some((
self.call_intrinsic("llvm.cos.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
"sin" => Some((
self.call_intrinsic("llvm.sin.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
"pow" => Some((
self.call_intrinsic("llvm.pow.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
"exp" => Some((
self.call_intrinsic("llvm.exp.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
"log" => Some((
self.call_intrinsic("llvm.log.f64", val_l, val_r),
Expr::Ident("f64".to_string()),
None,
)),
_ => None,
}
}
// Compile binary comparison operations. Returns boolean i1 value.
"==" | "!=" | "<=" | ">=" | "<" | ">" => {
let compiled_args: Vec<BasicValueEnum> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap().0;
val
})
.collect();
let v = match name.as_str() {
"==" => Some(
self.build_eq(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
"!=" => Some(
self.build_neq(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
"<" => Some(
self.build_lt(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
">" => Some(
self.build_gt(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
"<=" => Some(
self.build_le(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
">=" => Some(
self.build_ge(compiled_args[0], compiled_args[1])
.as_basic_value_enum(),
),
_ => None,
};
Some((
v.expect("Unsupported comparison operator"),
Expr::Ident("bool".to_string()),
None,
))
}
// Bitwise Operations
//and,or,xor,left shift,right shift(sign extend is true),right shift(sign extend is false)
"&" | "|" | "^" | "<<" | ">>" | "l>>" => {
let compiled_args: Vec<(
BasicValueEnum,
Expr,
Option<VariablesPointerAndTypes>,
)> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
val
})
.collect();
let val_l = match compiled_args[0].0 {
BasicValueEnum::IntValue(v) => v,
_ => panic!("{} require i values", name.as_str()),
};
let val_r = match compiled_args[1].0 {
BasicValueEnum::IntValue(v) => v,
_ => panic!("{} require i values", name.as_str()),
};
// compiled_args[0].clone().1 is type of first argument
// return type is type of first argument
match name.as_str() {
"&" => Some((
self.builder
.build_and(val_l, val_r, "and_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
"|" => Some((
self.builder
.build_or(val_l, val_r, "or_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
"^" => Some((
self.builder
.build_xor(val_l, val_r, "xor_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
"<<" => Some((
self.builder
.build_left_shift(val_l, val_r, "<<_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
">>" => Some((
self.builder
.build_right_shift(val_l, val_r, true, ">>_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
"l>>" => Some((
self.builder
.build_right_shift(val_l, val_r, false, "l>>_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
)),
_ => panic!("Unsupported bitwise operator: {}", name),
}
}
// Bool Operations
// && = and, || = or
"&&" | "||" | "and" | "or" => {
let compiled_args: Vec<(
BasicValueEnum,
Expr,
Option<VariablesPointerAndTypes>,
)> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
val
})
.collect();
let lhs_i1 = match compiled_args[0].0 {
BasicValueEnum::IntValue(v) if v.get_type().get_bit_width() == 1 => v,
_ => panic!("{} require bool values", name.as_str()),
};
let rhs_i1 = match compiled_args[1].0 {
BasicValueEnum::IntValue(v) if v.get_type().get_bit_width() == 1 => v,
_ => panic!("{} require bool values", name.as_str()),
};
let v = match name.as_str() {
"&&" | "and" => Some(
self.builder
.build_and(lhs_i1, rhs_i1, "and")
.unwrap()
.as_basic_value_enum(),
),
"||" | "or" => Some(
self.builder
.build_or(lhs_i1, rhs_i1, "or")
.unwrap()
.as_basic_value_enum(),
),
_ => None,
};
Some((v.unwrap(), Expr::Ident("bool".to_string()), None))
}
// Bit Reverse
"!" | "not" => {
let compiled_args: Vec<(BasicValueEnum, Expr, Option<_>)> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
val
})
.collect();
let val_i1 = match compiled_args[0].0 {
BasicValueEnum::IntValue(v) => v,
_ => panic!("{} require int or bool values", name.as_str()),
};
Some((
self.builder
.build_not(val_i1, "not_tmp")
.unwrap()
.as_basic_value_enum(),
compiled_args[0].clone().1,
None,
))
}
// Cast to numeric types
// char, i32/i64, f32/f64 -> cast to i64 or f64
// String -> parsed to i64 or f64
"as_i64" | "as_f64" => {
let compiled_args: Vec<(BasicValueEnum, Expr, Option<_>)> = args
.into_iter()
.map(|arg| {
let val = self
.compile_expr(arg, variables)
.expect("argument must have value");
val
})
.collect();
match name.as_str() {
"as_i64" => Some((
self.build_cast_to_i64(compiled_args[0].0)
.as_basic_value_enum(),
Expr::Ident("i64".to_string()),
None,
)),
"as_f64" => Some((
self.build_cast_to_f64(compiled_args[0].0)
.as_basic_value_enum(),
Expr::Ident("f64".to_string()),
None,
)),
_ => None,
}
}
// Deprecated names for backward compatibility
// They behave the same as "as_i64" | "as_f64"
// Kept to allow older code to continue working
"parse_i64" | "parse_f64" => {
let compiled_args: Vec<(BasicValueEnum, Expr, Option<_>)> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
val
})
.collect();
match name.as_str() {
"parse_i64" => Some((
self.build_cast_to_i64(compiled_args[0].0)
.as_basic_value_enum(),
Expr::Ident("i64".to_string()),
None,
)),
"parse_f64" => Some((
self.build_cast_to_f64(compiled_args[0].0)
.as_basic_value_enum(),
Expr::Ident("f64".to_string()),
None,
)),
_ => None,
}
}
// Pure if expression: if(cond, then_val, else_val)
// Both 'then_val' and 'else_val' are always evaluated.
// Avoid side effects in either branch.
"if" => {
let compiled_args: Vec<(BasicValueEnum, Expr, Option<_>)> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
val
})
.collect();
Some((
self.build_if_expr(
compiled_args[0].0,
compiled_args[1].0,
compiled_args[2].0,
"if",
),
compiled_args[1].clone().1,
None,
))
}
// Print with newline
"println" => {
let s_val = self.compile_expr(&args[0], variables).unwrap();
let str_ptr = match s_val.0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("println expects string pointer"),
};
self.build_println_from_ptr(str_ptr);
None
}
// Print without newline
"print" => {
let s_val = self.compile_expr(&args[0], variables).unwrap();
let str_ptr = match s_val.0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("print expects string pointer"),
};
self.build_print_from_ptr(str_ptr);
None
}
// Wrapper of sprintf
"format" => {
// string pointer
let fmt_val = self.compile_expr(&args[0], variables).unwrap();
let fmt_ptr = match fmt_val.0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("format expects string pointer"),
};
// Remaining arguments: values to format
let arg_vals: Vec<_> = args[1..]
.iter()
.map(|a| self.compile_expr(a, variables).unwrap().0)
.collect();
// Returns a string pointer containing the formatted string
let res_ptr = self.build_format_from_ptr(fmt_ptr, &arg_vals);
Some((
res_ptr.into(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![Expr::Ident("char".to_string())],
},
None,
))
}
// Pointer addition in 1-byte units (like void* + n)
"ptr_add_byte" => {
// args: [ptr, idx] or [idx, ptr]
let val_l = self.compile_expr(&args[0], variables).unwrap();
let val_r = self.compile_expr(&args[1], variables).unwrap();
let (ptr_val, idx_int) = match (val_l.0, val_r.0) {
(BasicValueEnum::PointerValue(p), BasicValueEnum::IntValue(i)) => {
(p, self.int_to_i64(i))
}
(BasicValueEnum::IntValue(i), BasicValueEnum::PointerValue(p)) => {
(p, self.int_to_i64(i))
}
_ => panic!("ptr_add expects (ptr, int) or (int, ptr)"),
};
// GEP using i8 as element type => 1 byte unit
let gep = unsafe {
self.builder
.build_gep(
self.llvm_type_from_expr(&Expr::Ident("T".to_string())), // 1byte
ptr_val,
&[idx_int],
"ptr_add_byte",
)
.unwrap()
};
// return as ptr:T (like void*)
// borrow checker doesn't check "ptr" type pointer
Some((
gep.as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![Expr::Ident("T".to_string())],
},
None,
))
}
// Pointer addition in units of the pointed-to type
// Returns the same pointer type as the left operand
"ptr_add" => {
// args: [ptr, idx] or [idx, ptr]
let val_l = self.compile_expr(&args[0], variables).unwrap();
let val_r = self.compile_expr(&args[1], variables).unwrap();
let (ptr_val, idx_int) = match (val_l.0, val_r.0) {
(BasicValueEnum::PointerValue(p), BasicValueEnum::IntValue(i)) => {
(p, self.int_to_i64(i))
}
(BasicValueEnum::IntValue(i), BasicValueEnum::PointerValue(p)) => {
(p, self.int_to_i64(i))
}
_ => panic!("ptr_add expects (ptr, int) or (int, ptr)"),
};
// GEP using the type pointed by left operand => adds in units of that type
let gep = unsafe {
self.builder
.build_gep(
self.llvm_type_from_expr(&expr_deref(&val_l.1)),
ptr_val,
&[idx_int],
"ptr_add",
)
.unwrap()
};
Some((gep.as_basic_value_enum(), val_l.1, None))
}
// for backward compatibility
// non-multidimensional version of []
"index" => {
// args: [ptr, idx] or [idx, ptr] -> load *(ptr + idx)
let a = self.compile_expr(&args[0], variables).unwrap();
let b = self.compile_expr(&args[1], variables).unwrap();
let (ptr_val, idx_int) = match (a.0, b.0) {
(BasicValueEnum::PointerValue(p), BasicValueEnum::IntValue(i)) => {
(p, self.int_to_i64(i))
}
(BasicValueEnum::IntValue(i), BasicValueEnum::PointerValue(p)) => {
(p, self.int_to_i64(i))
}
_ => panic!("index expects (ptr,int) or (int,ptr)"),
};
let gep = unsafe {
self.builder
.build_gep(
self.llvm_type_from_expr(&expr_deref(&a.1)),
ptr_val,
&[idx_int],
"idx_ptr",
)
.unwrap()
};
let loaded = self
.builder
.build_load(self.llvm_type_from_expr(&expr_deref(&a.1)), gep, "idx_load")
.unwrap();
Some((
loaded.as_basic_value_enum(),
expr_deref(&a.1),
Some(VariablesPointerAndTypes {
ptr: gep,
typeexpr: expr_deref(&a.1),
}),
))
}
// index access
// [](arr , e1 ,e2 ,...) = arr[e1][e2]...
"[]" => {
let depth = args.len(); // number of indices + 1 (for array pointer)
let arr_pointer = self.compile_expr(&args[0], variables).unwrap();
let ptr_val = match arr_pointer.0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("[] expect ptr,i64,i64,... found {:?}", arr_pointer.1),
};
let mut last_ptr = ptr_val;
let mut typeexp = arr_pointer.1;
for i in 1..depth {
let (val, ty, _p) = self.compile_expr(&args[i], variables).unwrap();
let idx_int = match val {
BasicValueEnum::IntValue(i_val) => self.int_to_i64(i_val),
_ => panic!("[] expect ptr,i64,i64,... found {:?}", ty),
};
let gep = unsafe {
self.builder
.build_gep(
self.llvm_type_from_expr(&expr_deref(&typeexp)),
last_ptr,
&[idx_int],
"idx_ptr",
)
.unwrap()
};
if i == depth - 1 {
last_ptr = gep;
break;
}
last_ptr = self
.builder
.build_load(
self.llvm_type_from_expr(&expr_deref(&typeexp)),
gep,
"idx_load",
)
.unwrap()
.into_pointer_value();
typeexp = expr_deref(&typeexp);
}
typeexp = expr_deref(&typeexp);
let loaded = self
.builder
.build_load(self.llvm_type_from_expr(&typeexp), last_ptr, "idx[]_load")
.unwrap();
Some((
loaded.as_basic_value_enum(),
typeexp.clone(),
Some(VariablesPointerAndTypes {
ptr: last_ptr,
typeexpr: typeexp.clone(),
}),
))
}
// Allocate memory on the stack
"alloc_array" | "alloc" => {
// args: [type_name, length_expr]
// type of elements
let elem_ty = self.llvm_type_from_expr(&args[0]);
// capacity = IntValue
let len_val = self.compile_expr(&args[1], variables).unwrap();
let len_val = match len_val.0 {
BasicValueEnum::IntValue(i) => i,
_ => panic!("alloc_array: length must be integer"),
};
// elem_ty must be IntType , FloatType or PointerType = bool,char,i32,i64,T,f64,f32,ptr,&,&mut,*
let array_ptr = self
.builder
.build_array_alloca(elem_ty, len_val, "array")
.unwrap();
// let array_ptr = match elem_ty {
// BasicTypeEnum::IntType(t) => self
// .builder
// .build_array_alloca(t, len_val, "array")
// .unwrap(),
// BasicTypeEnum::FloatType(t) => self
// .builder
// .build_array_alloca(t, len_val, "array")
// .unwrap(),
// BasicTypeEnum::PointerType(t) => self
// .builder
// .build_array_alloca(t, len_val, "array")
// .unwrap(),
// _ => panic!("alloc_array: unsupported type"),
// };
// Return as ptr:elem_ty
Some((
array_ptr.as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![args[0].clone()],
},
None,
))
}
"free" => {
self.compile_free(&args[0], variables);
None
}
// Transfer ownership of *:T variable
"pmove" => {
let (val, ty, ptr) = self.compile_expr(&args[0], variables).unwrap();
match &ty {
// Expect variable identifier
Expr::TypeApply { base, args: _ } if base == "*" => {
if let Expr::Ident(var_name) = &args[0] {
// Check if variable has already been moved
if let Some(owned) = self.current_owners.get_mut(var_name) {
if !*owned {
println!(
"{} : at pmove \"{}\" already moved",
"Error".red().bold(),
var_name
);
}
*owned = false;
}
if let Some(owned) = self.scope_owners.owners[self.scope_owners.pos]
.get_mut(var_name)
{
*owned = false;
}
}
}
Expr::TypeApply { base, args: _args } if base == "&" || base == "&mut" => {
println!(
"{} pmove expect *:T variables found {:?}",
"Error".red().bold(),
&ty
);
}
_ => {
println!(
"{} pmove expect *:T variables found {:?}",
"Error".red().bold(),
&args[0]
);
}
}
Some((val, ty, ptr))
}
// immutable borrow
"p&" => {
let (val, ty, ptr) = self.compile_expr(&args[0], variables).unwrap();
Some((
val,
Expr::TypeApply {
base: "&".to_string(),
args: vec![expr_deref(&ty)],
},
ptr,
))
}
// mutable borrow
"p&mut" => {
let (val, ty, ptr) = self.compile_expr(&args[0], variables).unwrap();
match &ty {
Expr::TypeApply { base, args: _args } if base == "&" => {
println!(
"{}: p&mut expect &mut:T or *:T found {:?} {:?}",
"Error".red().bold(),
ty,
&args[0]
)
}
_ => {}
}
Some((
val,
Expr::TypeApply {
base: "&mut".to_string(),
args: vec![expr_deref(&ty)],
},
ptr,
))
}
// malloc(size,elements_type)
// malloc require pointed type for type safety
// this is safety wrapper of C malloc
"malloc" => {
let compiled_size = self.compile_expr(&args[0], variables).unwrap();
let size = match compiled_size.0 {
BasicValueEnum::IntValue(i) => Some(i),
_ => panic!("malloc expects integer size"),
};
let ele_type = self.llvm_type_from_expr(&args[1]);
// return ptr:elements_type
Some((
self.compile_malloc(size.unwrap(), ele_type)
.as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![args[1].clone()],
},
None,
))
}
// realloc(ptr ,size,elements_type)
// realloc require pointed type for type safety
// this is safety wrapper of C realloc
"realloc" => {
let ptr = match self.compile_expr(&args[0], variables).unwrap().0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("realloc expects (ptr,size,type)"),
};
let compiled_size = self.compile_expr(&args[1], variables).unwrap();
let size = match compiled_size.0 {
BasicValueEnum::IntValue(i) => Some(i),
_ => panic!("realloc expects (ptr,size,type)"),
};
let ele_type = self.llvm_type_from_expr(&args[2]);
// return ptr:elements_type
Some((
self.compile_realloc(ptr, size.unwrap(), ele_type)
.as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![args[2].clone()],
},
None,
))
}
// memcpy(dest,src,size)
"memcpy" => {
let dest_ptr = self.compile_expr(&args[0], variables).unwrap();
let src_ptr = self.compile_expr(&args[1], variables).unwrap();
let size = self.compile_expr(&args[2], variables).unwrap();
let (dest_i8, src_i8, size_int) = match (dest_ptr.0, src_ptr.0, size.0) {
(
BasicValueEnum::PointerValue(d),
BasicValueEnum::PointerValue(s),
BasicValueEnum::IntValue(i),
) => (
self.builder
.build_pointer_cast(
d,
self.context.ptr_type(AddressSpace::default()),
"dest8",
)
.unwrap(),
self.builder
.build_pointer_cast(
s,
self.context.ptr_type(AddressSpace::default()),
"dest8",
)
.unwrap(),
i,
),
_ => panic!(
"memcpy expects (ptr,ptr,int) found {:?},{:?},{:?}",
dest_ptr.1, src_ptr.1, size.1
),
};
self.builder
.build_memcpy(dest_i8, 1, src_i8, 1, size_int)
.unwrap();
None
}
// memmove(dest,src,size)
"memmove" => {
let dest_ptr = self.compile_expr(&args[0], variables).unwrap();
let src_ptr = self.compile_expr(&args[1], variables).unwrap();
let size = self.compile_expr(&args[2], variables).unwrap();
let (dest_i8, src_i8, size_int) = match (dest_ptr.0, src_ptr.0, size.0) {
(
BasicValueEnum::PointerValue(d),
BasicValueEnum::PointerValue(s),
BasicValueEnum::IntValue(i),
) => (
self.builder
.build_pointer_cast(
d,
self.context.ptr_type(AddressSpace::default()),
"dest8",
)
.unwrap(),
self.builder
.build_pointer_cast(
s,
self.context.ptr_type(AddressSpace::default()),
"dest8",
)
.unwrap(),
i,
),
_ => panic!(
"memmove expects (ptr,ptr,int)found {:?},{:?},{:?}",
dest_ptr.1, src_ptr.1, size.1
),
};
self.builder
.build_memmove(dest_i8, 1, src_i8, 1, size_int)
.unwrap();
None
}
// memmove(dest,value,size)
"memset" => {
let dest_ptr = self.compile_expr(&args[0], variables).unwrap();
let src_val = self.compile_expr(&args[1], variables).unwrap();
let size = self.compile_expr(&args[2], variables).unwrap();
let (dest_i8, value, size_int) = match (dest_ptr.0, src_val.0, size.0) {
(
BasicValueEnum::PointerValue(d),
BasicValueEnum::IntValue(v),
BasicValueEnum::IntValue(s),
) => (
self.builder
.build_pointer_cast(
d,
self.context.ptr_type(AddressSpace::default()),
"dest8",
)
.unwrap(),
v,
s,
),
_ => panic!("memset expects (ptr,val,int)"),
};
let value_i8 = self
.builder
.build_int_truncate(value, self.context.i8_type(), "val8")
.unwrap();
self.builder
.build_memset(dest_i8, 1, value_i8, size_int)
.unwrap();
None
}
//return type size in byte as i64
"sizeof" => Some((
self.compile_sizeof(&args[0]),
Expr::Ident("i64".to_string()),
None,
)),
// === struct member access ===
// "_>": struct pointer -> member value (dereference pointer, then load member)
// ".": struct value -> member value (use alloca of struct value, then load member)
// "->": struct pointer -> member pointer (get pointer to member, do not load)
// struct member access
// struct pointer -> member value
"_>" => {
let value_struct = self.compile_expr(&args[0], variables).unwrap();
let (memberptr, membertype) =
self.compile_member_access(value_struct.0, &args[1], &value_struct.1);
let member_value = self
.builder
.build_load(
self.llvm_type_from_expr(&expr_deref(&membertype)),
memberptr,
"getmembervalue",
)
.unwrap();
Some((
member_value.into(),
expr_deref(&membertype),
Some(VariablesPointerAndTypes {
ptr: memberptr,
typeexpr: expr_deref(&membertype),
}),
))
}
// struct member access
// struct value -> member value
"." => {
let value_struct = self.compile_expr(&args[0], variables).unwrap();
let alloca = value_struct.2.unwrap().ptr;
let (memberptr, membertype) = self.compile_member_access(
alloca.as_basic_value_enum(),
&args[1],
&Expr::TypeApply {
base: "ptr".to_string(),
args: vec![value_struct.1],
},
);
let member_value = self
.builder
.build_load(
self.llvm_type_from_expr(&expr_deref(&membertype)),
memberptr,
"getmembervalue",
)
.unwrap();
Some((
member_value.into(),
expr_deref(&membertype),
Some(VariablesPointerAndTypes {
ptr: memberptr,
typeexpr: expr_deref(&membertype),
}),
))
}
// struct member access
// struct pointer -> member pointer
"->" => {
let value_struct = self.compile_expr(&args[0], variables).unwrap();
let (memberptr, membertype) =
self.compile_member_access(value_struct.0, &args[1], &value_struct.1);
Some((memberptr.as_basic_value_enum().into(), membertype, None))
}
// wrapper of C scanf
"scanf" => {
let fmt_val = self.compile_expr(&args[0], variables).unwrap();
// Remaining arguments: values to format
let fmt_ptr = match fmt_val.0 {
BasicValueEnum::PointerValue(p) => p,
_ => panic!("scanf expects string pointer"),
};
let arg_vals: Vec<_> = args[1..]
.iter()
.map(|a| self.compile_expr(a, variables).unwrap().0)
.collect();
self.build_scan_from_ptr(fmt_ptr, &arg_vals);
None
}
// cast to generic pointer
"to_anyptr" => {
// to_anyptr(ptr)
let ptr = self.compile_expr(&args[0], variables);
match ptr.unwrap().0 {
BasicValueEnum::PointerValue(p) => Some((
self.compile_to_anyptr(p, variables).as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![Expr::Ident("T".to_string())],
},
None,
)),
_ => None,
}
}
// cast from generic pointer
"from_anyptr" => {
// from_anyptr(ptr,pointed type)
let ptr = self.compile_expr(&args[0], variables);
let target_type = self.llvm_type_from_expr(&args[1]);
match (ptr.unwrap().0, target_type) {
(BasicValueEnum::PointerValue(p), BasicTypeEnum::PointerType(target)) => {
Some((
self.compile_from_anyptr(p, target).as_basic_value_enum(),
Expr::TypeApply {
base: "ptr".to_string(),
args: vec![args[1].clone()],
},
None,
))
}
_ => None,
}
}
// Call other functions
name => {
// Lookup LLVM function by name.
let func = self
.module
.get_function(&name)
.expect(&format!("Function {} not found", name));
// Compile each argument and convert to BasicMetadataValueEnum,
// which is required by build_call.
let compiled_args: Vec<BasicMetadataValueEnum> = args
.into_iter()
.map(|arg| {
let val = self.compile_expr(arg, variables).unwrap();
BasicMetadataValueEnum::from(val.0)
})
.collect();
// Emit LLVM call instruction
let call_site = self
.builder
.build_call(func, &compiled_args, "calltmp")
.unwrap();
// If the function has a return value, extract it.
if func.get_type().get_return_type().is_some() {
Some((
call_site.try_as_basic_value().basic().unwrap(),
// return type is stored in function_types map (AST-level type)
self.function_types
.get(name)
.expect(&format!("not defined function {}", name))
.clone(),
None,
))
} else {
None // return is void
}
}
},
_ => None,
}
}
}
お疲れ様です。この本でのWapLコンパイラの作り方の説明は一旦ここまでにしておきます。ソースコードはhttps://github.com/kazanefu/WapL_Compilerで公開されているので興味があれば見てみてください。もし問題点を見つけたり改善をしてくれたら遠慮なく報告していただけるとありがたいです。
Clang/LLVM -21のインストール(Ubuntu系想定)
以下のコマンドを順に打って最後にclang-21 --versionでバージョンが出力されたら成功です。
$ sudo apt update
$ sudo apt install -y wget gnupg
$ wget https://apt.llvm.org/llvm.sh
$ chmod +x llvm.sh
$ sudo ./llvm.sh 21
$ clang-21 --version
この方法でインストールした場合プロジェクト内のwapl.tomlのclangの欄を"clang-21"に書き変えるか以下のようにしてclangをclang-21になるようにしておく必要があるかもしれません
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-21 100
WapLでWebAssemblyにビルドする
環境構築
waplc_v0.2以降ではWebAssembly(以下wasm)向けビルドに対応しています。またwapl-cliが古いとwapl-cliでのwasm向けビルドに対応していないことがあるのでその場合は第1章のWapLを導入にある最初のコマンドで更新してその後waplup updateでwaplcの更新をしてください。
まずはwasm向けにビルドするためにまずはwasmに対応したclangや.wasmから.watファイルを作るツールをインストールします、すでにこれらがインストールされている場合はwapl.tomlファイルに設定を書くところまで飛ばして大丈夫です。
wgetがなければインストール
$ sudo apt update
$ sudo apt install -y wget gnupg
wasi-sdkをインストール(バージョンはLLVM-21に対応しているもの)
$ wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-linux.tar.gz
$ tar xf wasi-sdk-29.0-x86_64-linux.tar.gz
wasmの実行のためのランタイムをインストール
$ wget https://github.com/bytecodealliance/wasmtime/releases/download/v40.0.0/wasmtime-v40.0.0-x86_64-linux.tar.xz -O wasmtime.tar.xz
$ tar -xf wasmtime.tar.xz
.watファイルを生成するためのツールをインストール
$ sudo apt install wabt
ではここからwapl.tomlに設定を書きます。ここの例は上記のインストール方法で環境を整えた場合の設定です
[wasm]
input = "src/main.wapl"
output = "target/プロジェクト名.wasm"
opt = "O3"
clang = "$HOME/wasi-sdk-29.0-x86_64-linux/bin/clang"
bitsize = "32"
sysroot = "$HOME/wasi-sdk-29.0-x86_64-linux/share/wasi-sysroot"
wasm2wat = "wasm2wat"
wat = "src/プロジェクト名.wat"
wasmruntime = "$HOME/wasmtime-v40.0.0-x86_64-linux/wasmtime"
memory-size = "655360"
それぞれの項目について説明します。
input,output,opt,clang,bitsize、に関しては[build]のときと同じです。clangのところはちゃんとwasmに対応したものに切り替えておきましょう。sysrootにはwasi-sdkのshare/wasi-sysrootのパス、wasm2watにはwasm2watのパス、watは生成される.watファイルの名前、wasmruntimeはwasmtimeのパス、memory-sizeにはwasmでのヒープmemoryのサイズです。
ここまで設定できたら
$ wapl-cli wasm
でビルド
$ wapl-cli wasm_run
でビルド&実行
$ wapl-cli wasm_browser
でエントリーポイントやlibc無しでビルド
のようにしてビルドすることができます。
wapl-cli wasmやwapl-cli wasm_runでビルドするときはエントリーポイントを作るのであれば__main_void():i32または__main_argc_argv(i32 argc, ptr:ptr:char argv):i32としてください。またwapl-cli wasm_browserではlibcとリンクして使っていたmallocやprintfなどの関数が使えなくなりますしそれらを内部で使っているため標準ライブラリのstdにあるものも一部使えません。さらにprintlnやformatなどの組み込み関数も内部ではlibcを使っているため使えなくなります。そのため代替となる関数を組み込み関数と名前衝突が起こらないように作って使ってください。
WASM向けビルドをして実行するまでのチュートリアル
とりあえずプロジェクトの作成からビルド&実行
$ cd ~/projects/
$ wapl-cli new hello_wasm
$ cd hello_wasm
まずhello_wasmプロジェクトを作ります。
次に自分の環境に合わせてwapl.tomlファイルに設定を書いてください。
次に./src/main.waplを以下のように書き変えてください。
fn __main_void():i32{
println("Hello, WASM!");
return 0s;
}
これで
$ wapl-cli wasm_run
とすることでビルド&実行されて
Hello, WASM!
と表示されるはずです。ポイントとしてはネイティブではmain():i32だったエントリーポイントが__main_void():i32になっていることです。
ブラウザで動くものを作ってみよう
ここではWapLをwapl-cli wasm_browserを使ってビルドしてWapLで作った関数をTypeScriptという言語から呼んでブラウザで表示するというものを作ってみましょう。プロジェクトは先ほど作ったhello_wasmを流用します。まず、Node.jsやTypeScriptの環境がない場合はその環境を構築します。以下の環境構築方法は筆者が行った例です。自分の環境に合わせた方法で行ってください。
$ cd ~
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.6/install.sh | bash
$ export NVM_DIR="$HOME/.nvm"
$ [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
$ nvm install --lts
$ nvm use --lts
$ node -v
$ npm -v
$ cd projects/hello_wasm/
$ npm init -y
$ npm install --save-dev typescript ts-node @types/node
$ tsc --init
$ npm install --save-dev vite
これでhello_wasmプロジェクトでNode.jsでTypeScriptを使う環境が整いました。以降はプロジェクトでnpm init -yとtsc --initさえすればいつでもNode.jsが使えるようになります。
次はWapLのWASM向けライブラリwasmplを取得します。
$ wapl-cli get_lib wasmpl 0.1.2
ここではwasmplのバージョン0.1.2を取得しましたが、今回使う機能はこの0.1.2時点である機能で十分なのでこのバージョンにしています。WapL標準ライブラリstdにあるものを順次wasmplにも同様のwasm版のを作っていっているのでそれらが使いたい場合はより新しいバージョンであればあるかもしれません。
次に./src/main.tsを作ります。ここにWapLの関数を呼び出すTypeScriptのコードを書いていきます。
次は./index.htmlを作ります。
ここからコードを書いていきます。このチュートリアルではWapLで整数nを受け取ってn項までのフィボナッチ数列の配列を返す関数を作って、それをTypeScriptから呼んで配列の要素すべてを画面上に表示するようなものを作ります。
index.htmlには以下のように書きます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WapL Fibo</title>
</head>
<body>
<h1>WapL Fibonacci</h1>
<label for="inputN">n = </label>
<input type="number" id="inputN" value="10" min="0" />
<button id="calcBtn">計算</button>
<pre id="output"></pre>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
./src/main.waplには以下のようにフィボナッチ数列を作るfibo(isize):*:isizeという形の関数を作ってexportすることでTypeScript側で呼べるようにします。
use "./lib/wasmpl/wasmpl_all.wapl" // wasmplにあるものをすべて使えるようにする
fn fibo(isize n):*:isize{
#=(sz,sizeof(isize),isize);
// 配列のためのメモリをヒープ上に確保
// ここでは組み込み関数のmallocがlibc依存のため使えないのでwasmplにあるmalloc_wasmを使う
#=(arr,malloc_wasm(*(n,sz)),*:isize);
if(<=(n,1_)){
=(arr,Array(1_));
return arr;
}
=(arr,Array(1_,1_)); // 初項
#=(i,2_,isize);
// フィボナッチ数列を計算
loopif:(<(i,n)){
=([](arr,i),+([](arr,-(i,1)),[](arr,-(i,2))));
=(i,+(i,1_));
}
// *:isizeの型で配列を返す
return pmove(arr);
}
export fibo; // exportして外から呼べるようにする
そしたら、TypeScriptでfiboを呼び出す処理を./src/main.tsに書き込みます。
async function main() {
const response = await fetch("/target/hello_wasm.wasm"); // WapLをビルドして作られた.wasmファイルを取得
const bytes = await response.arrayBuffer();
const imports = { env: {} };
const wasmModule = await WebAssembly.instantiate(bytes, imports);
const exports = wasmModule.instance.exports as any; // exportされているWapLの関数を取得
exports.__init_wasm(); // wasmplにあるmalloc_wasmやfree_wasmを使っているためメモリの初期化とかをするために__init_wasmを呼ぶ必要がある
const memory = exports.memory as WebAssembly.Memory;
const inputN = document.getElementById("inputN") as HTMLInputElement; // nの入力する場所を指定
const calcBtn = document.getElementById("calcBtn")!; // 計算ボタンを指定
const output = document.getElementById("output")!; // 結果を表示する場所を指定
calcBtn.addEventListener("click", () => {
const n = parseInt(inputN.value);
if (isNaN(n) || n < 0) {
output.textContent = "0以上の整数を入力してください";
return;
}
// WapLで作ったfibo を呼び出す
const ptr = exports.fibo(n) as number;
// Int32Array を作ってコピー
const memView = new Int32Array(memory.buffer, ptr, n);
const result = Array.from(memView);
// 描画
output.textContent = `fibo(${n}) = [${result.join(", ")}]`;
// メモリ解放
exports.free_wasm(ptr);
});
}
main();
これでコードは書き終わったのでビルドしてブラウザで開いてみましょう。
WapLのビルド
$ wapl-cli wasm_browser
全体のビルド&実行
$ npx vite
これで表示されるURLをブラウザで開けば以下のように表示されるはずです。
計算ボタンを押すと
のように表示することができるものが作れました。