VCSSLの主な文法・仕様

前回は、VCSSLプログラムの実行と、基本的な入出力について扱いました。 今回は、VCSSLの主な文法と仕様について、まとめて見てみましょう。 即席ガイドという趣旨から、読者がすでにC系の言語の知識があるとの前提に立って、簡潔に駆け足でまとめていきます。

なお、解説はVCSSLのコードで行いますが、特に断りのない限り、サブセット言語であるVnanoでも同じ解説が成り立ちます。

- 目次 -

変数と型、演算

変数の宣言

変数の宣言は以下のようにします:


int a;              // 宣言のみ
int b = 1;          // 宣言と同時に初期化
const int N = 100;  // 変更できない変数(定数)
VariableDeclear.vcssl

変数はグローバル領域でもローカル領域でも宣言できます。 ただしグローバル変数は同じ型なら重複宣言が許されますが、ローカル変数では許されません。 また、グローバル変数は宣言行の前であっても参照できますが、ローカル変数は宣言行の後でしか参照できません。 なお、constの付いたグローバル定数は、全ての行の実行よりも先に初期化されます。

なお、Vnanoでは、上述のようにグローバル変数に対して特別扱いする機能は削られており、全てローカル変数と同じ扱いになります。

プリミティブ型

基本的なプリミティブ型は int / float / complex (Vnanoでは未対応) / string / bool の5種です。この他にも多倍長拡張型などがありますが、この5種だけ覚えておけば普通に使うには十分です:


int a = 1;                // 整数型
float f = 2.3;            // 浮動小数点数型
complex c = 4.0 + 5.0*I;  // 複素数型(Iは虚数単位定数) ※Vnanoでは未対応
string s = "Hello";       // 文字列型
bool b = true;            // 論理型

print(a, f, c, s, b);
VariableType.vcssl

実行すると以下の通り表示されます:

1 2.3 (4.0,5.0) Hello true

と表示されます。

これらの型は、他の様々な言語でもよく見かけるものですが、VCSSLでは以下の点に留意が必要です:

プリミティブ型の演算

各プリミティブ型での演算ルールについて、留意が必要な特徴を簡単にまとめておきましょう。

まず int、float、complex型については、各種の四則演算や比較演算が行えます。四則演算結果の型は以下の通りです:

なお、complex型では四則演算は可能ですが、大小比較は行えません。

string型は文字列を扱う型で、参照型ではなく値型として振る舞います。VCSSLでは string 型の値の加算が可能で、以下の通りです:

bool型は論理型(真偽型)で、論理演算が行えます。VCSSLでは比較演算の結果はbool型で、if文などの条件式は、必ずbool型でなければいけません。

配列

続いて配列ですが、最初に一つ注意が必要な点があります。VCSSLの配列は、多くのC系言語と異なり、参照型ではなく値型として振る舞います (VCSSLでは、GCを不要にするため、参照型が存在しません)。つまり、配列同士の代入演算は、参照の代入ではなく、全要素の値のコピーとなります。同じデータ領域を参照するようにはなりません。

使い方は以下のような形式になっています:


int a[3];  // int[3] a; も可能

a[0] = 0;
a[1] = 1;
a[2] = 2;

print(a);
Array.vcssl

実行結果は「0 1 2」です。多次元配列では以下のように宣言します:


int a[2][3];
Array2D.vcssl

配列は宣言と同時に初期化も可能です(Vnanoでは不可):


int a[ ] = {0, 1, 2};
ArrayInit.vcssl

上のように宣言時に要素数を省略した場合、初期化しなければ要素数0の配列となります。

配列の要素数を取得するにはlength関数を使用します:


int a[3];

int n = length(a, 0);  // aの要素数を取得

print(a);
ArrayLength.vcssl

実行結果は「3」です。length関数の第2引数は次元インデックスで、例えば2次元なら:


int a[2][3];

int n0 = length(a, 0);
int n1 = length(a, 1);

print(n0, n1);
ArrayLength2D.vcssl

のように、要素数を知りたい次元(左から0, 1, 2, …)を指定します(実行結果は「2 3」)。

配列の要素数は、alloc関数で動的に変更できます:


int a[2];   // a の要素数は 2

a[0] = 0;
a[1] = 1;

alloc(a, 3);   // a の要素数を 3 に変更

a[2] = 2;

print(a);
ArrayAllocFunction.vcssl

実行結果は「0 1 2」です。要素数変更後も、もともとあった要素の値は保たれます。 つまりC言語で例えると、malloc というよりも realloc に近いイメージです。

構造体(Vnanoでは使用不可)

型の紹介の最後として、構造体です:


//構造体の宣言
struct Box {
    int width;
    int height;
}

//構造体変数の宣言と使用
Box box;  //structは不要、というより付けてはいけない

box.width = 100;
box.height = 200;

println(box.width, box.height);
StructBox.vcssl

実行すると以下の通り出力されます:

100 200

ここで構造体「変数」の宣言時に、C言語のようにstructキーワードを付けてはいけない点に要注意です。 structキーワードを付けるのは、最初の構造体の型宣言の場所だけです。

なお、構造体も値型で、参照型にすることはできません。構造体変数の代入は、全メンバの値のコピーとなります。 また、構造型はNULL許容型で、NULLとの比較と代入ができます。禁止する機能は今のところありません。

ところで、現在のVCSSL処理系の実装では、構造体はメモリ・処理速度共にハイコストです。 現状、要素数が多い配列や、高速に回る箇所では、あまり積極的に使うことはお奨めできません。 将来的に、次の世代の処理系では改善される予定です。

制御構文

if 文 ( if-else 文 )

VCSSLでは、制御構文は if( if-else )/ for / while の3種のみです。まずはif文の例です:

実行すると入力値で分岐してメッセージが出力されます。条件式はbool型限定です。


int a = input("整数を入力してください",100);

if(a >= 10) {
    print("10以上です");
} else if(a >= 5) {
    print("5以上です");
} else {
    print("4以下です");
}
If.vcssl

なお、VCSSLでは、if / else / for / whileのぶら下がり構文を禁止しているため、ブロックスコープの { } は省略できません。以下のように省略するとエラーとなります:


if(a >= 10) print("10以上です");  //これはエラーとなる
IfError.vcssl

ところでC言語の文法的な話に鋭い人からは、以下のような指摘があるかもしれません:

ぶら下がり構文が禁止ならば、else の後に if がぶら下がっている「else if」という書き方はおかしいのでは? elifのような別の構文が必要では…

これはその通りで、VCSSLではelseの後にifがぶら下がっているのではなく、特例的に2語の「else if」で一つの構文キーワードになっています。

※ これは、以下の2つの観点の兼ね合いによる仕様です: これら2つの要請がまずあって、文法は後付けで調整された、という具合です。

for 文

続いてfor文の例です:


int a = input("整数を入力してください",10);

for(int i=1; i<=a; i++) {
    println(i);
}
For.vcssl

実行すると、入力値以下の正の整数を出力します。 ただしコンソールへの出力処理は、演算処理よりも時間がかかるため、ループ回数を増やすと、出力完了までに時間がかかります。

while 文

続いてwhile文の例です。for文の例と同じ処理を記述すると以下のようになります:


int a = input("整数を入力してください",10);

int i=1;

while(i<=a) {
    println(i);
    i++;
}
While.vcssl

関数

関数の宣言

VCSSLでは、関数の宣言は基本的にC系言語の標準的な形式を踏襲しています:


int add(int a, int b) {
    return a+b;
}

int c = add(1, 2);

print(c);
Function.vcssl

実行すると「 3 」と出力されます。

配列を引数や戻り値とするには、次のように記述します:


int[ ] add(int a[ ], int b[ ]) {

    ...
FunctionArray.vcssl

オーバーロード

VCSSLの関数はオーバーロードが可能です。 つまり、引数の型や個数が異なる関数は、別のものとして扱われるため、識別子(関数名)が競合しても問題ありません。 なお、シグネチャが完全に競合してしまっている場合は、後に宣言されたほうの関数が有効になります。

引数の参照渡し

引数の参照渡しも可能で、具体的には名前の前に「&」を付けて:

のように宣言すればOKです。ただし後者の、「配列の参照渡し」の書き方は C++ と少し違う(C++だと (&a)[] になる)ので、注意が必要です。

たまに見かけるかもしれない変な引数宣言

なお、定数宣言でも登場した const キーワードを付加して、以下のように宣言する事で、「引数を参照渡しで受け取るが、値は書き換えない」という宣言が可能です:

これは一見すると、わざわざ参照渡ししてもらう意味が無さそうに思えますが、パフォーマンスや最適化の面で有利になる場合があります(不利になる場合もある)。 なので、標準ライブラリの引数宣言などでたまに見かけるかもしれませんが(これとか)、普通はあまり気にしなくてOKです。

動的解釈 (Vnanoでは使用不可)

インタプリタ形式の処理系では、eval 関数により、実行時に動的な式の解釈が行える事がよくありますよね。VCSSLでも一応可能です:


string expr = "1+2";

int a = eval(expr);  //文字列を式として動的評価

print(a);
EvalExpr.vcssl

実行すると「3」と出力されます。レキシカルスコープで、代入を行う事も可能です:


string expr = "a=1+2";

int a;

eval(expr);

print(a);
EvalStat.vcssl

eval関数の戻り値はstring配列で、intやfloatの非配列に代入するには変換コストがかかるため、ループ時などでは後者のほうが高速です。

なお、ループ時のevalの構文解析コストについては、処理系側で色々とキャッシュされるため、ほとんどありません。 普通にコード内に式を直接書いた場合と大差ないパフォーマンスが得られます。

※ よく知られた話ですが、eval 関数はセキュリティ上、扱いに注意を要します。ザルなネットワークや検査不能な経路から、不特定の第三者によって入力される内容を、eval に渡すのは絶対にやめましょう。入力者が悪意を持っていたら非常に危ないからです。

使える用途は基本的に、

ファイル入出力

数値計算などでは必須となるファイル入出力についても、簡潔にまとめておきます。

簡易ファイル入出力

まずは最も単純な、簡易ファイル入出力です。あまり複雑な事はできませんが、簡単です:


save("test.txt", "Hello World"); //書き込み

string s = load("test.txt");  //読み込み

println(s);
SimpleIO.vcssl

実行すると、「test.txt」というファイルが生成されて「Hello World」と書き込まれ、さらにそれが読み込まれてコンソールに表示されます。

なお、適当な箇所で改行したければ、書き込み内容に定数EOLを挟んでください:


save("test.txt", "Hello" + EOL + "World"); //書き込み
SaveWithEOL.vcssl

実行すると、改行を挟んで「Hello」「World」と書き出されます。 EOLは環境依存の改行コードを表す文字列定数です。もし、いわゆるLF(0x0A)を書き出したければ定数LFを、CR(0x0D)を書き出したければ定数CRを使用してください。

基本的なファイル入出力

もう少し高度なファイル出力の基本形は、以下のようになります:


int file = open("test.txt", "w");  //書き込みモード「w」でファイルを開く

writeln(file, "Hello");  //Helloと書き込んで改行
writeln(file, "World");  //Helloと書き込んで改行

close(file);  //ファイルを閉じる
Writeln.vcssl

実行すると、改行を挟み「Hello」「World」書かれたファイル「test.txt」が生成されます。

open関数はファイルを開き、複数のファイルを識別するための番号を割り振って返します。第2引数の "w" は書き込みモードを意味します。追記モードなら "a" とします。 これらのモードは、定数 WRITE や APPEND を用いても指定可能です。

writeln関数は、ファイルに書き込んで改行する関数で、改行を行わないwrite関数も存在します。使い方は同じで、第1引数が対象ファイルの番号、第2引数以降(可変長)が書き込み内容です。

続いてファイル入力です。基本形は以下の通りです:


int file = open("test.txt", "r");  //読み込みモード「r」でファイルを開く

int n = countln(file);  //行数をカウント

for(int i=0; i<n; i++){
    string line = readln(file);  //ファイルから1行読み込む
    println(line);               //コンソールに表示
}

close(file);  //ファイルを閉じる
Readln.vcssl

「test.txt」と同じ場所で実行すると、その内容がコンソールに出力されます。

open 関数の第2引数の "r" は読み込みモードを意味します。こちらも、代わりに定数 READ で指定する事もできます。

readln 関数は、ファイルから1行の内容を読み込む関数です。 実は readln 関数が返すのは配列なのですが、"r" モードの時は行内容をそのまま要素数 1 で返し、このように非配列変数( line )で受け取れます。

数値CSV形式や数値TSV形式のファイル入出力

open 関数は、数値CSV/TSV形式のファイルを扱えます。まずは書き込みです:


int file = open("test.csv", "wcsv");  //CSV書き込みモードでファイルを開く

writeln(file, 0, 1, 2);  //数値をコンマ区切りで書き込んで改行
writeln(file, 3, 4, 5);  //数値をコンマ区切りで書き込んで改行

close(file);  //ファイルを閉じる
WritelnCSV.vcssl

実行すると、ファイル「test.csv」に、改行を挟んで「0,1,2」「3,4,5」と書き出されます。 なお、"wcsv" の代わりに定数 WRITE_CSV も指定可能です。

続いて読み込みです:


int file = open("test.csv", "rcsv");  //CSV読み込みモードでファイルを開く

int n = countln(file);  //行数をカウント

for(int i=0; i<n; i++){
    string line[ ] = readln(file);  //ファイルからコンマ区切りで1行読み込む
    println(line);
}

close(file);  //ファイルを閉じる
ReadlnCSV.vcssl

実行すると先ほどの「test.txt」を読み込み、改行を挟んで「0 1 2」「3 4 5」と出力します。このように、rcsvモードでのreadln関数は、1行をコンマ区切りで読み込み、配列で返します。 なお、こちらも "rcsv" の代わりに定数 READ_CSV でも指定可能です。

ここで扱ったwcsv / rcsv モードでは数値CSV形式ですが、同様にwtsv / rtsv モードで数値TSV形式も扱えます。 ただし、これらの機能は、あくまで「数値」を対象とする、単純なものであるという点に注意が必要です。 文字列をCSV形式やTSV形式で正しく扱うには、file.TextFileライブラリ( https://www.vcssl.org/ja-jp/lib/file/TextFile )を使用してください。

ライブラリの読み込み (Vnanoでは搭載ソフト側の個別の方法参照)

文法面は大体まとめ終えたので、この節の最後に、ライブラリの扱いについて説明しておきます。

最初にライブラリを用意

まずは、以下の内容のファイルを「 TestLib.vcssl 」という名前で、「 testdir 」というディレクトリ(フォルダ)の中に作ってください:

- ./testdir/TestLib.vcssl -

void fun( ) {
    println("Hello");
}
TestLib.vcssl

include (Vnanoでは使用不可)

上で作ったライブラリを読み込みます。C/C++などではincludeがありますが、これはVCSSLでも使えます。 先ほどのtestdirディレクトリ(フォルダ)と同じ場所のプログラムから読み込むには:


include "./testdir/TestLib.vcssl" ;

fun( );
IncludeLib.vcssl

実行すると「Hello」と出力されます。

このように、includeの後には、「そのファイルから見た相対パス」を " " で囲って記述します。 includeはC/C++と同じく、ライブラリのコードがその箇所にそのまま展開されます。 しかしよく知られる通り、識別子の競合や多重includeなどの面倒もあります。 従って、VCSSLでは積極的には使いません(どうしても必要な場面のみ)。

import (Vnanoでは使用不可)

includeで生じる面倒を回避した、より高度なライブラリ読み込み方法として、importも使用できます。 VCSSLでは基本的にincludeよりもimportが推奨です:


import testdir.TestLib ;

fun( );
ImportLib.vcssl

実行すると「Hello」と出力されます。 このように、importの後には、「実行するファイル(ライブラリではない)から見た相対パス」を、スラッシュ区切り( / )の代わりにドット区切り( . )で指定します。

モジュールと名前空間 (Vnanoでは使用不可)

importで読み込んだライブラリは、includeと違ってその場に展開はされず、独立な形で読み込まれます。 この独立なプログラム単位をモジュールと呼び、多重importしても唯一性が確保されます。

全てのモジュールは互いにグローバル領域を参照できますが、暗黙的な名前空間も持ちます。 複数モジュールで識別子(変数名や関数名)が競合している場合、自身のモジュールのものが優先的に参照されますが、 所属名前空間を指定する事により、別モジュールのものも参照できます:


import testdir.TestLib ;

void fun( ) {
    println("World");
}

TestLib.fun( );  // 名前空間を明示すればTestLibのfunが呼ばれる

fun( );          // 名前空間を明示しないとこのモジュールのfunが呼ばれる
ModuleNamespace.vcssl

実行すると改行区切りで「Hello」「World」と出力されます。

標準ライブラリ

VCSSLには、他の多くの言語と同様、基本的な処理群は標準ライブラリとして同梱されています。標準ライブラリの一覧と詳細仕様は、以下のWebページで参照できます:

- VCSSL 標準ライブラリ 仕様書 -
    https://www.vcssl.org/ja-jp/lib/

なお、実はこれまでの解説でも使用していた標準ライブラリがあります。 それは「 System 」ライブラリで、例えばprint / println関数などもSystemライブラリが提供しています。 Systemライブラリは、特例的に全モジュールよりも先に自動で読み込まれるため、import不要で使用できます。

今回で、このガイド前半の基本編は終わりです。次回からは後半の応用編です。次回は、標準ライブラリの中から、実際に GUI と 2D / 3D グラフィックスを扱ってみます。