はじめに
この記事は「言語実装 Advent Calendar 2021」 23日目の記事です。もともとアドベントカレンダーに投稿予定では無かったのですが、この自作言語のリファレンスの更新が出来たタイミングでカレンダーが空いていたため便乗させていただきました。
Palanとは
Palanとは、より簡単で安全なC言語の代替となりうる言語を目指して、少しづつC++で開発しているプログラム言語(コンパイラ)です。いろいろ思う所ありLLVMは使用していません。まだまだ文法は簡素ですが、ある程度実用的なプログラム(サンプル: テトリス, レイトレース)も書けます。ver0.4では、新たに配列や構造体のメモリを直接スタックやメンバーに配置できるようになりました。これによりプログラムの高速性とC言語との互換性が向上しました。
また、簡易レジスタ割り付けも行っているため、現バージョンでコンパイルしたプログラムは、多くのケースでC言語(gcc)最適化なしのプログラムよりも高速に動作します。
Qiitaに開発日記を公開しています。
プログラミング言語Palan開発日記
プログラミング言語Palan開発日記 2年目
[プログラミング言語Palan開発日記 3年目]
(https://qiita.com/tosyama/items/104ccfa7cada1bf801af)
[プログラミング言語Palan開発日記 4-5年目]
(https://qiita.com/tosyama/items/34e747cc32e486b0e7fc)
設計思想・方針
明確さと簡易さを両立させた言語を目指しています。また、感覚的なことになりますが開発者本人にとっての書き味のよさを重要視しています。
サンプルプログラムと解説
Palanでの簡単なクイックソートのプログラムを紹介します。
ccall printf(...);
// init data.
const N = 10;
[N]int16 data = [0,4,8,3,7,2,6,1,5,0];
printf("before:");
show(data);
func show([N]int16 data)
{
i=0;
while i<N {
printf(" %d", data[i]);
i+1 -> i;
}
printf("\n");
}
printf("after:");
data ->> quicksort(0, N-1)
->> data
-> show();
func quicksort([N]int16 >>data, int32 left, right)
-> [N]int16 data
{
if left >= right { return }
var mid, i = left, left +1;
while i <= right {
if data[i] < data[left] {
mid++;
data[mid], data[i] -> data[i], data[mid];
}
i++;
}
data[left], data[mid] -> data[mid], data[left];
data ->> quicksort(left, mid-1)
->> quicksort(mid+1, right)
->> data;
}
コンソールから下記のように、pac
コマンドでコンパイルして実行ファイルを作成できます。実行ファイルを実行すると、ソート前とソート後の数字の並びが表示されます。
$ bin/pac quicksort.pa -o a.out
linking: a.out
$ ./a.out
before: 0 4 8 3 7 2 6 1 5 0
after: 0 0 1 2 3 4 5 6 7 8
オプションを指定しない場合は、コンパイル後、すぐに実行されます。
$ bin/pac quicksort.pa
before: 0 4 8 3 7 2 6 1 5 0
after: 0 0 1 2 3 4 5 6 7 8
プログラムの解説
Palanのファイルの拡張子は.pa
となります。
mainのようなスタート関数はなく、基本的に上から順に実行されます。
ccall printf(...);
ver0.4のPalanではデフォルトで標準Cライブラリとリンクします。ccall
で宣言することでライブラリの任意の関数を使えるようになります。...
は可変長引数を表しています。
// init data.
const N = 10;
ソートする配列の要素数の定数宣言です。定数はスコープ内であればどこでも使えます。値としてはリテラルおよびリテラル同士の計算が指定できます。
//
はコメントです、行末まで無視されます。
[N]int16 data = [0,4,8,3,7,2,6,1,5,0];
ソート対象となる配列変数の宣言と初期化です。int16
は16bit整数、[]
は配列であることを示しています。配列は[]
の中にカンマ区切りで数値を書くことで初期化できます。ここでは配列をソート対象の数字列で初期化します。
printf("before:");
show(data);
C標準ライブラリのprintf
をコールして、コンソールに文字を表示します。show()
は配列の中の数字をコンソールに表示するPalanの関数コールです。show
関数の定義は後にあります。スコープ内であれば、関数の定義と呼び出しを記述する順番は前後しても構いません。
func show([N]int16 data)
{
配列の中を表示する関数を定義しています。
[N]int16 data
と定義された引数は、コピーされた配列が引数に渡りますので、関数の中で配列を変更しても呼び出し元の配列には影響がありません。
i=0;
while i<N {
printf(" %d", data[i]);
i+1 -> i;
}
printf("\n");
i
の変数宣言をしていますが、型の宣言が省略されています。省略した場合、右辺(=
より右)の型より推測されます。上記の場合はi
はint64
型になります。
while
は繰り返し文で、i<N
の条件をみたす限り {}
内の文を繰り返します。->
は代入を表し、左の値を右の変数に代入します。他の言語では代入に=
を使うことが多いのですが、Palanでは=
は初期化時のみで、代入は->
を使うところが一つの特徴になります。
printf("after:");
data ->> quicksort(0, N-1)
->> data
-> show();
data
配列をquicksort()
関数に渡してソートを実行し、結果を表示します。代入と同じ->
で関数の引数に値を設定して関数コールできます。また、代入と関数コールを->
で繋げることができ、Palanではチェーンコールと呼んでいます。->>
は、配列の所有権を渡します。所有権を渡した変数は所有権を得るまで使えません。
func quicksort([N]int16 >>data, int32 left, right)
-> [N]int16 data
{
ソートを行う関数を定義しています。
[N]int16>> data
と定義された引数は、所有権を渡す必要があります。所有権渡し(Move)はアドレスを渡すためコピー処理がなく高速です。-> int16[N] data
が戻り値の定義となります。関数は変数data
のリファレンスを返しますので、呼び出し元は所有権を取り戻すことができます。
if left >= right { return }
if
は、直後に書かれた条件を満たした時にのみ{}
の中の処理を実行します。ここではleft
がright
以上の場合、return
文で関数を終了し呼び出し元にもどります。
{}
の中の文が1つだけの場合は行末の;
は省略できます。
data[mid], data[i] -> data[i], data[mid];
,
で区切ることで複数の変数の代入ができます。ここではこの複数の代入を使って配列の要素を入れ替えています。
ここまででクイックソートのプログラムの解説を終わります。
Palanの雰囲気が少しは感じられたでしょうか? その他の実際に動くサンプルプログラムは、リポジトリに置いています。
動作環境
Palanコンパイラpacは下記の環境で動作します。
- CPU: X86-64 Intel系 CPU
- Memory: 2GB
- OS: Ubuntu 18.04.1 LTS (64bit)以降
- Library: g++ (gcc 7.3.0/as/ld)以降, libboost-program-options
インストール
ver0.4ではインストール用のバイナリファイル/パッケージには用意していません。githubよりソースをクローンしてビルドする必要があります。ビルド方法はREADME.mdをお読みください。
https://github.com/tosyama/palan
コマンドラインリファレンス
Palanコンパイラ(pac)はコンソールからコマンドラインで使用します。
- ヘルプの表示
$ pac -h
コマンドラインのオプションが確認できます。
- バージョン情報の表示
$ pac -v
- コンパイルしてアセンブリを表示
$ pac -S foo.pa
標準出力にアセンブリが出力されます。
- コンパイルしてオブジェクトファイルを作成
$ pac -c foo.pa
foo.o
がカレントディレクトリに作成されます。オブジェクトファイル名はソースコードの拡張子を除いたファイル名+.o
となります。
※ オブジェクトファイルはld
でリンクすることで実行ファイルにできますが、複数のオブジェクトファイルの結合は今のPalanではサポートできてません。
- コンパイルして実行ファイルを作成
$ pac -o foo foo.pa
上記の例では、foo.o
というオブジェクトファイルと、foo
という実行ファイルが生成されます。
- コンパイルして、すぐに実行。
$ pac foo.pa
a.out
という名称の実行ファイルを生成して実行します。実行後、オブジェクトファイルと実行ファイルは削除されます。
コメント
//
の記述以降はコメントとなります。記述は行末まで無視され、プログラムとはみなされません。
// コメントです。
基本型
ver0.4では整数型と浮動小数点型が使用できます。
型名 | 説明 | 最小値 | 最大値 |
---|---|---|---|
sbyte | 符号付き8bit整数 | -128 | 127 |
byte | 符号なし8bit整数 | 0 | 255 |
int16 | 符号付き16bit整数 | -32,768 | -32,767 |
uint16 | 符号なし16bit整数 | 0 | 65535 |
int32 | 符号付き32bit整数 | -2147483648 | 2147483647 |
uint32 | 符号なし32bit整数 | 0 | 4294967295 |
int64 | 符号付き64bit整数 | -9223372036854775808 | 9223372036854775807 |
uint64 | 符号なし64bit整数 | 0 | 18446744073709551615 |
flo32 | 32bit浮動小数点 | (+-)1.175494e-38 | (+-)3.402823E+38 |
flo64 | 64bit浮動小数点 | (+-)2.225074e-308 | (+-) 1.797693e+308 |
変数
変数宣言
変数を使用するには事前に宣言する必要があります。
型名の後に変数名を書くことで変数を宣言します。変数はスコープ内({
と}
で囲まれた領域)で使用できます。
int32 i; // 32bit整数の変数iの宣言
int16 x, y; // 16bit整数の変数 xとyの宣言
byte b, sbyte sb; // 符号なし8bit整数bと符号あり8bit整数sbの宣言
=
を使って宣言時に変数を初期化できます。複数の変数宣言の場合は、=
の右側に,
で区切って初期値となる式を記述します。
int32 i = 0;
int16 x, y = 3, 4;
byte b, sbyte sb = 1u, -1;
x + y -> i; // iに3+4の結果が入る
型推論
変数宣言の初期化時に、型をvar
と指定すると右辺の型と同じにすることができます。宣言する変数が1つの場合はvar
も省略することができます。
int32 i = 10;
var i2 = i; // i2はint32
i3 = 10; // i3はint64
f = 1.23; // fはflo64
var x, y = 1.0, i; // xとyはflo64
var xx, var yy = 1.0, i; // xxはflo64、yyはint32
配列変数
Palanでは固定長配列を使用できます。配列変数を使用するには事前に宣言する必要があります。
[]
内に要素数を記述します。,
で区切ることで多次元配列の宣言もできます。指定する要素数は固定値である必要があり、整数リテラル、定数およびそれ同士の計算に限られます。
使用するときは[]
に0から"要素数-1"までのインデックスを指定します。インデックスには任意の式を指定できますが、範囲を超えないようにしてください。範囲を超えて指定した場合はクラッシュの原因になります。
[10]int32 a1, a2; //要素数10の32bit整数の配列変数1 a1, a2の宣言
[3,4]int32 m; //3x4の2次元配列の宣言。メモリの並びは要素4の配列が3つ。
// 要素へのアクセス
1 -> a1[0]; 10 -> a2[9];
12 -> m[2,3];
配列の配列
[]
を繋げることで配列の配列も使用できます。多次元配列と異なり、要素となる配列とその参照の配列が生成されます。
[10][3,4]int32 arrayOfMat; // [3,4]int32を要素とする要素数10の配列
123 -> arrayOfMat[9][2,3];
[3,4]int32 m = arrayOfMat[9]; // 2次元配列として扱われる
配列表現
配列表現を使うと、配列を初期化したり無名の配列を作成したりすることができます。配列表現は一連の値を,
で区切り[]
で囲んで表します。配列表現の配列に値を代入したり、配列表現から所有権を取得することはできません。
多次元配列は配列表現を並べて書くことで表現できます。入れ子形式でも書くこともできます。
[3]int32 a = [1,2,3]; // aを初期化
i = 10;
[i,i+1,i+2]->a; // aに代入
[2,3]int32 a2 = [1,2,3][4,5,6]; //多次元配列
[2,3]int32 a3 = [[1,2,3],[4,5,6]]; // 入れ子形式の記述も可能
[2][3]int32 aa = [1,2,3][4,5,6]; //配列の配列
基本的に配列表現の型は代入先の型になりますが、型推論の際は多次元配列とみなされます。(配列の配列にはなりません)
a = [1,2,3][4,5,6]; // aの型は[2,3]int64
要素数の省略
配列変数の初期化時に要素数を省略すると、右辺の値の要素数と同じになります。ただし、次元数は一致している必要があります。
[]int32 a = [1,2,3]; // aの型は[3]int32
[,]flo32 f = [1,2][3,4]; // fの型は[2,2]flo32
[,3]int32 a2 = [1,2,3][4,5,6]; // 一部省略も可
[][]int32 aa = [1,2,3][4,5,6]; // 配列の配列も可
要素数未定
C言語関数との配列データの受け渡しを行う場合など、要素数が不明な場合があります。その場合は?
を使うことができます。下記はC言語関数の宣言の引数で指定している例です。
ccall strlen(@[?]byte str) -> int32;
オブジェクト要素の直接配置
配列や後述する構造体など、オブジェクトを要素とする場合、デフォルトではヒープ領域にメモリ確保され、配列の要素にはそのアドレスへの参照が入ります。要素の型名の前に$
をつけるとオブジェクトの要素を配列内のメモリに直接配置することができます。
[2]$[3]int32 a; // メモリ配置は[2,3]int32と同じ
構造体
複数のデータを一つの変数としてまとめるのには構造体を使います。構造体を使用するには型名とその内容を定義する必要があります。type
の後に型名、{}
の中にメンバーを記載し構造体を定義します。
type User {
int64 id;
[80]byte name;
int16 age;
};
User userA; // 構造体の変数userAを宣言
オブジェクト要素の直接配置
配列や構造体など、オブジェクトを要素とする場合、デフォルトではヒープ領域にメモリ確保され、メンバーにはそのアドレスへの参照が入ります。要素の型名の前に$
をつけると、オブジェクトを構造体内のメモリに直接配置することができます。
type User {
int64 id;
$[80]byte name; // 構造体内に直接配置される
int16 age;
};
メンバーへのアクセス
変数名に.
とメンバー名を続けて記述することで、構造体メンバーにアクセスできます。
User userA;
// 構造体メンバーに値を代入
101 -> userA.id;
"Alice" -> userA.name;
12 -> userA.age;
// 構造体メンバーの値を取得して表示
printf("%d: %s %d\n", userA.id, userA.name, userA.age); // 101: Alice 12
リテラル表現
構造体の代入や初期化には配列表現を使うこともできます。
User userA = [101, "Alice", 12];
User userB;
[102, "Bob", 13] -> userB;
スタック領域への配置
Palanでは配列や構造体などのオブジェクトはデフォルトではヒープ領域にメモリを確保します。処理によってはこのオブジェクトのメモリ確保や開放処理がパフォーマンス上のボトルネックとなることがあります。
その場合は、スタック上にオブジェクトを確保することでメモリの確保や開放処理を無くして高速化することができます。型名の前に$
をつけることで配列や構造体をスタック領域に直接配置できます。スタック上にメモリを配置する際はスタックオーバーフローに注意して下さい。
$[3]int32 a = [1,2,3]; // 配列aをスタック領域に確保
$User u = [101, "Alice", 12];
型エイリアス
型の別名(エイリアス)を定義することができます。別名をつけることでより用途が分かりやすくなることがあります。
type
の後に別名、続いて=
とエイリアスしたい型名を記述します。
type ID = int64;
type Point3D = [3]flo64;
ID id = 10;
Point3D p = [1.2, 1.3, 4.5];
読み取り専用リファレンス
配列、構造体を関数等に受け渡す際はメモリの確保、コピー処理等のコストが必要ですが、リファレンスを使用するとアドレスのみの受け渡しですむのでパフォーマンスを向上させることができます。またC言語のポインタの代替としても使えます。リファレンス先の型名の前に@
をつけたものが型名となります。@
のリファレンスは初期化後は読み取り専用となり、書き込みはできません。
type struct_tm { int32 tm_sec; };
ccall strlen(@[?]byte str) -> int32;
ccall localtime(@uint64 t) -> @struct_tm;
@[?]byte str = "read only string";
len = strlen(str);
uint64 t = time(0);
@struct_tm tm = localtime(t);
型名のみの宣言
型の名前だけ宣言することもできます。直接変数を宣言して使用することはできませんが、C言語ライブラリの型をポインタで扱う場合に便利です。
type FILE; // 型名のみの宣言
ccall fpoen(@[?]byte filename, mode) -> FILE;
ccall fprintf(@FILE stream, @[?]byte format, ...) -> int32;
ccall fclose(FILE >>stream);
FILE f <<= fopen("file.txt", "w");
f->fprintf("Hello\n");
fclose(f>>);
式
値を返す構文を式と呼びます。式は;
で区切られるまで繋げて一つの文(ステートメント)を形成できます。
リテラル
ソースコード中に固定値を直接記述できます。
名称 | 説明 | 例 |
---|---|---|
整数リテラル | 整数の固定値です。符号付64bit整数として扱われます。10進数で記述します。 | -12345 12345 |
符号なし整数リテラル | 正の整数の固定値です。符号なし整数として扱われます。10進数の後ろにu を付けます。 |
12345u |
小数リテラル | 小数の固定値です。64bit浮動小数点数として扱われます。整数部と小数部を. で区切り記述します。その後ろにe で始まる指数部も記述できます。 |
123.4 1.234e2 |
文字列リテラル | 文字列の固定値です。基本的にbyte配列として扱われます。byte配列への代入、関数の引数、定数等で使えます。" で囲みます。エスケープシーケンスが使えます。 |
"Hello World!\n" |
配列リテラル | 全ての要素がリテラルか定数で構成された固定の配列表現です。通常の配列とは違い、定数の右辺や仮引数のデフォルト値としても使用できます。 | [1,2,3] |
変数
変数宣言した変数名を記述するとその変数の値が返ります。代入、算術演算、関数引数などで使用できます。
代入
代入演算子->
を使うと計算結果などを変数に格納できます。左辺から右辺に値がコピーされます。
複数の変数に代入することもできます。
121+2 -> i; // 123をiに代入
i + 3 -> i; // 126をiに代入
123, 456 -> i, j; // 複数の代入。123をiに、456をjに代入
f() -> i, j; // 関数fが複数の値を返し、それぞれ代入できる
複数の代入を使って値を入れ替えることもできます。
int32 i, j = 1, 2;
i,j -> j,i; // 値の入れ替え。iは2、jは1となる。
配列変数や構造体、配列の配列に代入演算子を使用すると全ての要素がコピーされます。(Deep Copy)
[10]int32 a, b;
User userA, userB;
...
a -> b; // 深いコピー
userA -> userB;
所有権の移動(Move)
配列、構造体などのオブジェクトに関して所有権が移動できます。所有権の移動にはMove演算子->>
を使います。2番目以降の変数にも移動したい場合は移動先の変数に>>
を付加します。配列の中身をコピーせずに参照のみを移動先の変数に渡すため、代入に比べて高速に値を渡せます。
所有権移動元の変数は、移動後は所有権を獲得するまで使用できません。もし使用した場合、クラッシュの原因となります。
[10]int32 a, b, c, d;
...
a ->> b; // 配列の中身の所有権をaからbへ移動
// 2->a[2]; // 使用不可
b, c ->> a, >>d; // aは所有権を取り戻すと同時にcからdに所有権を移動
[10]int32 e <<= a; // 所有権を移動して初期化
Moveは主に関数との大きなオブジェクトの受け渡しで使用します。配列変数同士の直接のMoveでは、受け渡し先が保持していたメモリは解放されます。
算術演算
整数や小数に対して下記の算術演算ができます。
演算子 | 名称 | 説明 | 例 |
---|---|---|---|
+ | 加算 | 足し算を行います。 | 1+2 -> i; // 3 |
- | 減算 | 引き算を行います。 | 4-3 -> i; // 1 |
* | 乗算 | 掛け算を行います。 | 3*2 -> i; // 6 |
/ | 除算 | 割り算の商を求めます。整数同士の場合、少数点は切り捨てられます。 | 10/3 -> i; // 3 |
% | 余算 | 割り算の余りを求めます。少数には使用できません | 10%3 -> i; // 1 |
- | 負 | 負数を返します。 | -i -> i; |
演算子には優先順位があります。*
, /
, %
は+
, -
よりも優先順位が高くなります。
基本的にリテラル同士の計算はコンパイル時に行われます。
比較演算
比較演算子を使って整数および小数の比較ができます。比較の結果が真の場合、1となります。偽の場合、0となります。式が使用できるところであればどこでも使えますが、if
文やwhile
文の条件として使用する機会が多いでしょう。
浮動小数点の比較では常に丸め誤差に気をつける必要があります。
演算子 | 名称 | 説明 | 例 |
---|---|---|---|
== | 等しい | 左値と右値が等しい場合1、それ以外の場合0になります。 | if a==10 {...} |
!= | 等しくない | 左値と右値が等しくない場合1、等しい場合0になります。 | if a!=10 {...} |
> | 大なり | 左値が右値より大きい場合1、等しいか小さい場合0になります。 | if a>10 {...} |
< | 小なり | 左値が右値より小さい場合1、等しいか大きい場合0になります。 | if a<10 {...} |
>= | 大なりイコール | 左値が右値より大きいか等しい場合1、小さい場合0になります。 | if a>=10 {...} |
<= | 小なりイコール | 左値が右値より小さいか等しい場合1、大きい場合0になります。 | if a<=10 {...} |
インクリメント/デクリメント演算子
インクリメント演算子を使うと、変数の値を1のみ増加させることができます。デクリメント演算子を使うと変数の値を1のみ減少させることができます。これらはステートメントとなるので式と組み合わせることはできません。
演算子 | 名称 | 説明 | 例 |
---|---|---|---|
++ | インクリメント | 変数に1加算します。 | i++; |
-- | デクリメント | 変数から1減算します。 | i--; |
整数と小数の演算
整数と小数が混在する演算を行う場合は、整数を64bit浮動小数点に昇格した上で計算や比較が行われます。
整数の昇格
整数型の変数は64bit整数に昇格して計算や比較が行われます。代入時に結果の上位ビットが切り捨てられます。
符号付き整数と符号なし整数の計算の場合は、符号なし整数は符号付き整数として扱われて計算されます。
注意: C言語と仕様が異なります。
条件演算
複雑な条件を判断するために下記の条件演算が使用できます。
演算子 | 名称 | 説明 | 例 |
---|---|---|---|
&& | かつ | 左値が0でない、かつ右値が0でない場合1、それ以外は0になります。左値が0であった場合、右値の式は計算されません。 | if a>0 && a <= 10 {...} |
¦¦ | または | 左値が0でない、または右値が0でない場合1、それ以外は0になります。左値が1であった場合、右値の式は計算されません。 | if a<0 ¦¦ a >= 10 {...} |
! | 否定 | 式が0の場合1、0でない場合1になります | if !(a<0) {...} |
関数の呼び出し
関数名と()の中に引数を指定することで宣言、定義されたスコープ内の関数を呼び出すことができます。スコープ内であれば呼び出し後に関数定義があっても構いません。
引数には所有権を要求するものがあります。その場合には所有権を手放すことを明確にするため>>
を引数の後ろに付加する必要があります。
戻り値は代入演算子、またはMove演算子を使って受け取ることができます。
戻り値を受け取ることは必須ではありません。受け取らなかった戻り値が使用していたメモリは自動解放されます。
int32 x, y;
[10]int32 a;
int32 result;
// 関数呼び出しの例
foo(); // 引数なし、戻り値を受け取らない。
foo(x,y) -> result; // 引数あり、戻り値あり。
foo(a>>, x, y)->>a, result; // 所有権移動の引数、複数の戻り値
foo(a) -> a; // 内容のコピーによる受け渡し
func foo() { ... } // 関数定義は後でもよい
func foo (int32 x, y) -> int32 {...}
func foo ([10]int32 >>a, int32 x, y) -> [10]int32 a {...}
func foo ([10]int32 a) {...}
...
オーバーロード
Palanでは関数名が同じで引数が異なる関数を定義できます(オーバーロード)。呼び出し時に引数の型と数が完全に一致するものがあればその関数が呼び出されます。
型が完全に一致しない場合、整数など暗黙に型変換した上で一致する関数が1つである場合、その関数が呼び出されます。2つ以上の関数に一致した場合はコンパイルエラーとなります。
引数の省略
関数の引数にデフォルト値が設定されている場合は、引数を省略することができます。引数を省略した場合はデフォルト値が使われます。
// xと、yの引数が省略可能な関数
func foo(int32 x=1, y=2) { }
foo(3,4); // 通常の呼び出し
foo(); // foo(1,2)
foo(3); // foo(3,2)
foo(,3); // foo(1,3);
チェーンコール
代入やMove演算子を使って引数を指定して関数呼び出しを行ったり、さらに関数の戻り値を次の関数呼び出しの引数に適用できます。
通常の関数呼び出しと比較し、関数を左から右に順番に記述できるので、データや処理の流れやを表現しやすくなります。
代入演算子の左値、または関数の戻り値は、引数の先頭から順に割り当てられ、関数が呼び出されます。
int32 a,b;
add(a,3)->b; // 通常の関数呼び出し
a->add(3)->b; // 代入(1つ)で引数指定
a,3 -> add() -> b; // 代入(2つ)で引数指定
add(add(a,3),9)->b; // 通常の関数呼び出し(ネスト)
a->add(3)->add(9)->b; // 関数の戻り値を次の関数へ渡す
func add(int32 a, b)->int32 c
{...}
Move演算子は最初の引数にのみ適用されます。2番目以降の引数に適用する方法は提供していませんので、その場合は通常の関数呼び出しを使用する必要があります。
[10]byte s1, s2;
s1 ->> mix(>>s2) ->> s1, >>s2;
// s1, s2 ->> mix(); // NG: 2番目の引数は代入と見なされる
func mix([10]byte >>a, >>b)
-> [10]byte a, b
{ // 省略
}
定数
const
キーワードを使用するとプログラム上で共通に使用する数値や配列を定数として定義できます。リテラルおよびリテラル同士の計算を指定できます。式の中で使う以外に、配列の型の宣言の要素数やデフォルト引数でも使用できます。
const M,N = 3,4;
const FORMAT = "bar: %d";
const LEN = M*N; // リテラル同士の計算
int32 l,z = LEN, Z; // 式。Zは定義前だが使用可能
func foo([M,N]int32 m) // 固定配列の宣言
{
const FORMAT = "foo:%d"; // 定数の上書き
printf(FORMAT, LEN); // 親のブロックで定義された定数LENは使用可能
}
const Z = LEN;
定数はスコープ内であれば定義前でも使用できます。ただし定数の定義中で別の定数使用する場合は、その定数は前に定義しておく必要があります。
子のブロックでは親ブロックの同じ名前の定数を定義すると上書きされます。
制御文
if
やwhile
等の制御文を使うことで、処理を分岐させたり繰り返したりして流れを制御できます。
トップレベル
関数定義の外に記述されたコードをトップレベルと呼びます。プログラムはトップレベルの上から順次実行され、トップレベルの最後に来ると終了します。
トップレベルではreturn
文は使用できません。任意の場所で終了する場合はC標準ライブラリのexit
関数等を使用してください。
ブロック
{}
で囲まれた領域がブロックです。ブロックを使用する事で、変数、関数、定数やその名前の有効な領域を制限できます。
通常、ブロック内で宣言した変数はそのブロックと子のブロック内で使用でき、ブロックの外側では使用できません。
処理がブロックを抜けると、ブロック内で宣言した変数が使用しているメモリは全て解放されますので、ブロックを適切に使用すると使用メモリを節約できます。
if文
条件によって処理を分岐させたい場合はif文を使用します。
if
文 は条件式を評価し、条件式が真(0以外)の場合は、直後のブロックを実行します。
if a==2 { // ifの後に、任意の条件式を記述
// 条件式が真(aが2)の場合に実行
}
条件式が偽(0)の場合に別の処理を行わせたい場合は、if
文のあとに else
文を記載します。
if a==2 {
// 条件式が真(aが2)の場合に実行
} else {
// 条件式が偽(aが2以外)の場合に実行
}
else if
文をつなげることで、複数の条件分岐ができます。
if a==2 {
// 条件式が真(aが2)の場合に実行
} else if a==3 {
// 条件式が真(aが3)の場合に実行
} else {
// 全ての条件式が偽(aが2と3以外)の場合に実行
}
while文
条件によって処理を繰り返す場合はwhile
文を使用します。while
文は条件式を評価し、条件式が真(0以外)である限り直後のブロックを実行し続けます。
無限ループによるCPUビジーの原因になりやすいため条件式のコーディングは特に気をつける必要があります。
int32 i=0;
while i<10 { // 条件式が真(iが10より小さい)の限り繰り返す
printf("%d",i);
i+1 -> i;
}
繰り返しの中断とスキップ
break
文を使用して、while
文による繰り返しを中断してブロックから抜ける事ができます。
また、continue
を使用すると、残りの処理をスキップして次の繰り返しを続けることができます。
通常、この2つの文はif
文と組み合わせて使うことになります。
i=0;
while i<10 {
if i==5 { break }
if i==3 { 4->i; continue }
printf("%d",i);
i+1 -> i;
}
// 出力は"0124"になる
関数
関数とは一連の処理をまとめたものです。入力を引数として受け取り、処理を行ったあと出力を戻り値として返します。Palanの関数定義では入力、出力を明確に表現できます。
関数定義
Palanの関数を定義するにはfunc
キーワード、関数名を記述し、その後に()
内に呼び出し元から受け取る仮引数を宣言します。
関数が戻り値を返す場合は、->
の後に戻り値を宣言します。
上記宣言の直後に関数で行う処理をブロックの中に記述します。
// 引数なし、戻り値なしの関数定義
func foo()
{
// ここに処理を記述
}
// 引数あり、戻り値なしの関数定義
func foo(int32 a, b, int16 c)
{
printf("a:%d, b:%d c:%d\n", a, b, c); // 仮引数は通常の変数として扱える
}
// 引数なし、戻り値ありの関数定義
func bar() -> int32 a, b, int16 c
{
// 戻り値も変数として扱える。
// 関数終了時の値が呼び出し元に返る。
1 -> a; 2-> b; 3->c;
}
// 引数あり、戻り値ありの関数定義
// 引数と、戻り値で同じ変数名aを指定可能。
func bar(int32 a, b) -> int32 a, c
{
1 -> c;
}
仮引数
関数呼び出し時に指定された引数の値は、仮引数に渡ります。
仮引数には型名と、変数名を記述します。複数の仮引数を宣言するには","で繋げます。前の引数と型が同じ場合は型名を省略できます。
通常、仮引数には呼び出し元の変数の値がコピーされます。仮引数の値を変更しても、呼び出し元の変数の値は変わりません。
call printf(...);
func foo(int32 i, j, sbyte b, [10]int32 a) // ()で囲まれた部分が仮引数の宣言
{
printf("before: i=%d, a[3]=%d¥n", i, a[3]);
8,9 -> i, j;
33 -> b;
88 -> a[3];
printf("after: i=%d, a[3]=%d¥n", i, a[3]);
}
// 引数に指定する変数
int32 ii, jj = 3, 4;
sbyte bb = 1;
[10]int32 arr;
99 -> arr[3];
foo(ii, jj, bb, arr); // 関数を呼び出し
printf("original: ii=%d¥n, arr[3]=%d", ii, arr[3]);
出力は下記のようになります。元の変数は変更されていません。
before: i=3, a[3]=99
after: i=8, a[3]=88
original: ii=3, arr[3]=99
仮引数の前に>>
を指定した場合、コピーではなく、呼び出し元の変数の所有権を要求します。参照が渡されるのでディープコピーは発生せず高速に受け渡しが可能です。一方、渡したままでは呼び出し元の変数は使用できなくなりますので、通常、呼び出し元に変更後の値を返す戻り値も宣言します。
仮引数の順番は、他の関数でも使うようなメインとなるデータを第一引数にして、オプション等を後ろにするとチェーンコールで使いやすくなります。
デフォルト引数
仮引数には=
でデフォルト値を指定することができます。デフォルト値を使用すると呼び出し元の引数指定の負担を減らすことができます。ただし、オーバーロードと併用した場合に、曖昧さが生じるとコンパイルエラーの原因になるので慎重に設計する必要があります。
デフォルト値としては、リテラルや定数などの固定値のみ使用できます。
// j, aに対してデフォルト値が指定されている
func foo(int32 i, j = 3, [2]int32 a = [1,2])
{
}
戻り値
処理の結果を呼び出し元に返すには戻り値を使用します。
戻り値は->
の後に型名と変数名を宣言します。複数の戻り値を宣言するには","で繋げます。前の引数と型が同じ場合は型名を省略できます。
func foo() -> int32 i, j, int16 l
{
1,2,3 -> i, j, l;
}
戻り値に指定した変数の関数終了時の値が、呼び出し元に返されます。
"%d, %d, %d\n", foo() -> printf(); // => 1, 2, 3
仮引数と同じ変数を戻り値として宣言することができます。その場合は呼び出し元の引数の値が変数代入されて呼び出され、そのまま戻り値にも使用されます。
func foo(int32 j) -> int32 i, j, int16 l // jは引数にも戻り値にも使用
{
j+1 -> j;
1,2 -> i, l;
}
"%d, %d, %d\n", foo(10) -> printf(); // => 1, 11, 2
型だけ指定し、変数名を指定しない書き方もできます。その場合はすべての戻り値を型のみの宣言にする必要があります。また、関数を終了する際は、return
文で戻り値をすべて記載する必要があります。
func foo() -> int32, int32, int16
{
return 1,2,3; // 必須
}
"%d, %d, %d\n", foo() -> printf(); // => 1, 2, 3
Return文
関数の任意の場所で処理を終了する場合はreturn
文を使用します。
return
文には呼び出し元に返す値を式で指定できますが、戻り値の宣言で変数名を指定している場合はその変数の値となるため返す値は不要です。
戻り値の宣言が型名だけの場合は必ず戻り値が必要です。
トップレベルの処理においてはreturn
文は使用できません。処理を終了する場合は標準Cライブラリのexit
関数等を使用してください。
入れ子関数
関数は入れ子で定義できます。入れ子になった関数は定義したスコープ内({}
で囲まれた中)でしか使用できません。大きな処理を処理を複数の関数に分割する際など、他から呼ばれたくない関数を定義したい場合などに使用できます。
mainfunc(); // OK
// subfunc() // NG
func mainfunc()
{
subfunc();
subfunc2();
func subfunc() {
}
func subfunc2() {
}
}
C言語関数の使用
Palanのプログラムは標準Cライブラリとリンクされます。ccall
宣言を記述するとprintf
などの標準Cライブラリの関数が使用できるようになります。C言語の型、int
/ long long
等は Palanの型 int32
, int64
等に置き換える必要があります。ポインタはリファレンスや出力引数に置き換える必要があります。C言語関数の宣言は基本的に以下の書式に従います。
ccall [関数名]([入力引数リスト]=>[出力引数リスト]) -> 戻り値の型名 : ライブラリ名;
C言語のヘッダーファイルの読み込みはサポートしていないため、関数、定数、構造体等、必要な定義はすべて自力で記述する必要があります。
戻り値
戻り値は->
の後に型名を1つ指定します。戻り値がない場合は->
以降の記述は不要です。
ccall strlen(@[?]byte str) -> int32; // 戻り値がある場合
ccall exit(int32 status); // 戻り値がない場合
ライブラリの指定
ライブラリをリンクしないと使えない関数は、明示的にライブラリ名を指定する必要があります。いずれかの関数宣言で使用するとリンクされますので、毎回指定する必要はありません。
ccall sqrt(flo64 x) -> flo64: m; // 数学ライブラリを指定
ccall pow(flo64 x, y) -> flo64;
可変長引数
C言語関数の可変長引数は...
で表現します。代表的な例ではprintf関数があります。
ccall printf(@[?]byte format, ...) -> int32;
出力引数とプレースホルダ
C言語では関数の引数が入力のみに使われるのか、出力として使われるのかどうかはconst
の有無で区別しますが、Palanでは=>
の前を、入力パラメータ、=>
の後は出力用パラメータと明確に区別されます。入出力兼用の場合は出力用とみなされます。=>
の前か後かで、関数実行後に値が変わる引数かどうかを容易に判断することができます。
出力パラメータの変数に配列等のオブジェクトを指定すると、そのポインタが渡されます。int64等整数を指定するとそのアドレスが渡されます。
関数側で確保したメモリのアドレスを受け取る場合(C言語におけるポインタのポインタ**
)には、Move演算子>>
を指定することができます。
// scanfの例
ccall scanf(@[?]byte format => ...) -> int32;
byte[32] str;
int32 num;
scanf("%31s %d" => str, num);
// sqliteの例
type sqlite3;
ccall sqlite3_open(@[?]byte filename => sqlite3 db>>) -> int32:sqlite3;
ccall sqlite3_close(sqlite3 >>db) -> int32;
sqlite3 db;
sqlite3_open(":memory:" =>> db);
db->>sqlite3_close();
sprintf関数など、出力にあたる引数が入力の前に定義された関数についてはプレースホルダ @
を用います。プレースホルダの位置に、=>
の後の引数が順番に指定されたとして扱われます。
// dstは本来はプレースホルダ @ の位置
ccall sprintf(@, @[?]byte src, ... => [?]byte dst) -> int32;
[10]byte str;
sprintf("%d", 123 => str);
書き込み可能リファレンス
ポインタを返す出力引数においては、Moveを使うことが不適切なことがあります。その場合は書き込み可能リファレンス@!
を使うことができます。書き込み可能リファレンスはアクセス制限がないため取り扱いに注意する必要があります。
ccall strtol(@[?]byte s, @, int32 base => @![?]byte endptr -> int32;
@![?]byte rest_str;
int32 l = strtol("1234 547", 10 => rest_str);
外部グローバル変数
C言語のライブラリ等で使用されているグローバル変数、例えばstdout
等を使用することができます。extern
識別子でグローバル変数を宣言します。
type FILE;
extern @FILE stderr;
stderr->fprintf("Error occured!\n");
システムコール
syscall
宣言をするとLinux(64bit)システムコールが使用できます。システムコールは任意の名前を指定でき、関数と同じように使用できます。宣言時にシステムコール番号と戻り値の型、対応する関数名を宣言します。
仮引数の宣言はありません。呼び出し時の引数がそのまま関数に渡されます。
syscall 1: write() -> int64;
syscall 60: exit();
注意: 上記のプログラム終了のシステムコールexit
を宣言して使用できますが、printf
などを使用している場合はバッファに出力が残ったまま終了してしまいます。printf
を使用する際は、標準Cライブラリのexit()
関数を使用した方がよいでしょう。
メモリ管理
Palanにはメモリ確保のnew
はありません。配列は宣言時に自動的にヒープ領域に確保されます。
GCやdelete
もありません。ヒープ領域に確保された領域はその所有権を持つ変数がスコープをはずれたり、別の所有権が移動された際に解放されます。
メモリ解放はGCとは違い即座に行われます。GCと比較して解放処理の分その処理速度が落ちることも考えられますが、常に安定したパフォーマンスと最小限のメモリを維持でき、突然のGCで急に処理が止まる(Stop the world)ことはありません。
トップレベルで確保された配列はプログラム終了時にOSより一度に解放されますので、個別の解放処理は行われません。
終わりに
まだまだ使える言語には程遠いですが、地道に開発を続けていきたいです。もし、興味をもっていただけて、一度でも使っていただければ幸いです。