Lua
言語処理系
lunescript

トランスコンパイラ LuneScript で Lua の開発をもっと楽に!!

Lua は非常にコンパクトな言語でありながら、高い潜在能力を持つ言語です。

プログラムに組み込まれる言語としては、最も使い易い言語の一つと言っても良いと思います。

ただ「プログラムに組み込まれる言語としては使い易い」とはいえ、イマドキの言語と比べると、いろいろと気になるところがあるのも事実です。

一方で、Lua をイマドキの言語に近づけるための機能進化は、「コンパクト」という Lua の大きな特徴の一つとトレードオフになる可能性があります。

そこで、 Lua 自体には手を加えずに、Lua の気になる箇所をカバー出来るトランスコンパイラ LuneScript を紹介します。

LuneScript とは

LuneScript とは、前述の通り Lua の気になる点をカバーする言語で、LuneScript で開発したコードを Lua のコードに変換することが可能なトランスコンパイラです。

LuneScript は、次の特徴を持ちます。

  • Lua と C の syntax を基調としているため、学習コストが低い。
  • 静的型付け言語であるため、型チェックにより単純なミスをコンパイル時に発見可能。
  • 型推論により、型宣言の手間を最小化。
  • NULL 安全 (null safety)。
  • generics (一部のみ)により、型情報を保ったままの処理が可能。
  • 言語の文法としてクラス定義を対応。
  • マクロ により、ポリモーフィズム等の動的処理に頼らないデザインを実現可能。
  • JSON と互換なデータ表現をサポート。
  • トランスコンパイルした Lua コードは、外部ライブラリを前提とせずに単体で動作可能。
  • トランスコンパイルした Lua コードは、LuneScript で書いた処理そのままが出力されるので、性能劣化がない。
  • 既存の Lua の外部モジュールを LuneScript から利用可能。
  • LuneScript は Lua 上で動作し、Lua 標準モジュール以外を必要としないため、導入が簡単。
  • Lua5.2, 5.3 をサポート。
  • LuneScript はセルフホスティングで開発している。

LuneScript の使用方法

LuneScript は github で開発しています。

https://github.com/ifritJP/LuneScript

開発中なためインストール手段を用意していませんが、前述の通り LuneScript は単なる Lua なので、src/lune/base/ 以下のスクリプトを適宜コピーするだけで動きます。

コマンド

LuneScript は、次のモードを持ちます。

  • LuneScript のコードを、Lua コードへトランスコンパイル
  • LuneScript のコードをそのまま実行

コマンドを実行するには、次を実行します。

$ lua lune/base/base.lua [-r] src.lns mode

ここで、 lune/base/base.lua は LuneScript をインストールした環境にあわせて適宜変更してください。

src.lns は、 LuneScript で作成したスクリプトのパスを指定します。拡張子は .lns です。

mode は次のいずれかをサポートします。

  • token
    • 字句解析結果を標準出力する。
  • ast
    • AST を標準出力する。
  • lua
    • Lua へのトランスコンパイルし、結果を標準出力する。
  • save

    • Lua へのトランスコンパイルし、結果を保存する。
    • トランスコンパイルしたファイルは、指定した lns ファイルと同じディレクトリに作成する。
  • exe

    • 実行する。

Lua へトランスコンパイルしたファイルは、Lua コマンドでそのまま実行できます。ただし、別の lns ファイルを import している場合は、その lns ファイルもトランスコンパイルしておく必要があります。

LuneScript の仕様

ここでは LuneScript の仕様について説明します。

補足記事は、ここにリンクを追加していきます。

値と型

LuneScript は次の値を扱います。

  • nil
  • 整数(int)
  • 実数(real)
  • 文字列(str)
  • 真偽値(bool)
  • リスト
  • マップ(Map)
  • 配列
  • クラス
  • マクロ
  • 関数
  • stem

nil

nil は、 Lua の nil と同じです。

LuneScript では null も利用できます。

null は nil の alias です。

null のサポートにより、 LuneScript で JSON をそのまま扱うことができます。

整数、 実数

LuneScript は、整数と実数を分けて扱います。

これにより 10/3 は 3 となり、 10/3.0 は 3.3333… となります。

数値リテラル

数値リテラルは C89 ライクなものを採用します。

  • 整数は 10 進数と 16 進数表現をサポート
  • 実数は 10 進数と e による指数表現。

追加で ASCII の文字コード表現が可能です。

let val = ?a;  // 0x61

上記のように ? に続く文字を ACSII コードに展開します。ASCII 以外の文字は対応しません。

' や " などの文字は、 ?\' のように \ でクオートする必要があります。

四則演算

数値の四則演算は Lua と同じものを採用します。

2項演算の結果は次の通り型が変わります。

  • int と int の演算結果は int になる。
  • real と real の演算結果は real になる。
  • int と real の演算結果は real になる。

ただし、 int と int の演算結果が int の範囲外になった場合、実行時の内部的な値としては real になりますが、LuneScript 上の型は int のままです。演算結果を int に丸めるには、 @@int でキャストする必要があります。

ビット演算

ビット演算をサポートします。Lua5.2 でも使用可能です。

ビット長は Lua5.2 では 32bit となります。Lua5.3 のビット長は、環境に依存します。

  • 論理積 (&)
1 & 3 == 1
  • 論理和 (|)
1 | 2 == 3
  • 排他的論理和 (~)
1 ~ 3 == 2
  • 論理シフト(左) (|<<)
1 |<< 2 == 4
  • 論理シフト(右) (|>>)
0x10 |>> 2 == 4
  • ビット反転 (~)
~2 == 0xfffffffd

文字列

文字列は Lua と同じで終端文字のないデータです。

文字列リテラルは " あるいは ' で囲みます。複数行の文字列リテラルは ``` で囲みます。

文字列内の N 番目の文字にアクセスするには txt[N] を使用します。ただし txt[N] は読み込み専用で、文字の書き換えは出来ません。

let txt = "1234";
txt[ 2 ] // ?2

また、Python に似た format 書式を利用可能です。

ここから〜
ここまで文字列```

"10 + %s = %d" ("1", 11) // "10 + 1 = 11"
```

文字列連結

文字列連結は Lua と同じ .. を使用します。

真偽値(bool)

true, false をもちます。

リスト、配列、マップ

LuneScript では、 Lua の table をリスト、配列、マップに分けて扱います。

リストは Lua のシーケンス、配列は固定長のリスト、マップは Lua の table です。

リテラルはそれぞれ次のように宣言します。

let list = [ 1, 2, 3 ];
let array = [@ 'a', 'b', 'c' ];
let map = { "A": 10, "B": 11, "C": 12 };

リスト、配列の最初の要素のインデックスは 1 です。

リスト、配列の長さを取得するには # を使用します。

let list = [ 1, 2, 3 ];
print( #list ) // 3

マップのサイズ(アイテムの個数)を取得することは出来ません。 ( Lua の制限 )

リスト

リストのオブジェクトは、順序付けて値を管理します。

let name : itemType[];

リストに保持できる値の型は、1 つに制限されます。ただし、後述する stem! 型のリストであれば、全ての値を保持できます。

例えば、次は int 型の要素を持つリストになります。

let name : int[];

リストのオブジェクトは、 insert、 remove メソッドを持ちます。

let mut list:int[] = [];
list.insert( 1 );  // [ 1 ]
list.insert( 2 );  // [ 1, 2 ]
list.insert( 3 );  // [ 1, 2, 3 ]
list.remove();     // [ 1, 2 ]

リストの要素にアクセスするには、次のように [N] で要素のインデックスを指定します。

let list = ['a','b','c'];
print( list[ 1 ] ); -- 'a'

要素のインデックスがリストの範囲外を指定した場合の処理は 未定義 です。

配列

配列オブジェクトは、固定長のリストです。サイズが固定であること以外はリストと同じです。

let mut list = [@ 1, 2 ];
list.insert( 1 );  // error

サイズ固定なため、 insert、 remove は出来ません。

マップ

マップのオブジェクトは、キーと値の紐付けを管理します。

let name : Map<keyType,valType>;

Map 型は、上記のように keyType と valType で宣言します。

例えば次の宣言は、キーが int 型で、値が str 型のマップです。

let val : Map<int,str>;

値にアクセスするには、次のように指定します。

let map = { "A": 10, "B": 11, "C": 12 };
print( map[ "A" ], map.B );

キーが文字列の場合、map.B のようにマップオブジェクトのメンバとしてアクセスできます。

マップオブジェクトのキー、値には nil を設定出来ません。

リスト、マップコンストラクタの型

let list = [ 1, 2, 3 ];
let map = { "A": 10, "B": 11, "C": 12 };

リスト、マップは、上記のようにリテラルを宣言できます。この時生成される リスト、マップの型は、 構成する値によって決まります。

マップコンストラクタで利用されるキー、あるいは値が全て同じ型なら、マップのキー、値の型は、そのキー、値の型になります。いずれかが異なれば stem 型になります。

具体的には、次のようになります。

let list1 = [ 1, 2, 3 ];                        // int[]
let list1 = [ 'a', 'b', 'c' ];                  // str[]
let list1 = [ 'a', 1, 'c' ];                    // stem[]
let map1 = { "A": 10, "B": 11, "C": 12 };       // Map<str,int>
let map2 = { "A": 10, "B": 11, "C": 12 };       // Map<str,int>
let map3 = { "a": 'z', "b": 'y', "c": 'x' };    // Map<str,str>
let map4 = { "a": 1, "b": 'Z' };                // Map<str,stem>

関数(form)

form は、関数を保持する型です。

例えば、次の test 関数の引数 func は、 form 型です。

fn test( func:form ) {
   func(); // "hoge"
}
test( fn () { print( "hoge" ); } );

この func 引数に関数オブジェクト fn () { print( "hoge" ); を与えており、test() 関数内で func() が実行されています。

なお form 型の変数は、次の型の関数として扱われます。

fn func(...):...;

stem

stem は、nil 以外の全ての値を保持できる型です。

LuneScript は、静的型付け言語であり、想定する型と異なる値を与えらた場合はコンパイルエラーします。

対して stem 型は、nil 以外の全ての型を扱える型なので、nil 以外のどのような値を与えられてもコンパイルエラーしません。

stem! は nil を含む全ての値を扱える型です。Lua の変数そのものと考えて問題ありません。

! 型 (nilable)

nilable は、 nil を保持可能な型です。逆に言えば、 nilable でなければ、nil は保持出来ません。これにより、非 nilable 型で扱っている間は、nil による実行時エラーに気を使う必要がありません。

型変換

一部の型の値は、型を変換することが出来ます。

変換するには次の書式を利用します。

val@@type

これは val の値を type に変換することを宣言します。

例えば、次は val の値を int に変換しています。

val@@int

数値型変換

数値型の値は異なる型に変換することが出来ます。変換には、丸めが発生します。

  • int から real
    • 整数から実数に変換
  • real から int

    • 実数から整数に変換
    • math.floor() を呼ぶのと等価。

stem 型との型変換

任意の型は stem 型と相互変換が可能です。

  • 任意の型から stem 型に変換
    • @@stem で明示せずに暗黙的に変換可能。
  • stem 型から任意の型に変換
    • @@type で明示が必要。
    • このとき、変換元の値が何の型だったかは判断しない。
    • 変換元の値の型と変換先の型が不一致した時の動作は 未定義

コメント

コメントは C++ スタイルを採用。一行コメント // 、 複数行コメント /* */ を指定可能。

// 行末までコメント
/* ここから〜
ここまでコメント*/

演算子

原則的に、演算子 は Lua と同じものを利用する。

Lua5.3 の //(切り捨て除算) は、1行コメントとなるので注意すること。

なお LuneScript では、整数同士の / は自動的に切り捨て除算となる。

変数宣言

[ pub | global ] let name [: type] = evp;

変数宣言は let で行なう。

let に続けて変数名を指定する。変数の型は変数名に続けて : を入れて型指定する。

ただし、変数宣言初期化の値から型が推測できる場合は、型指定を省略できる。

例えば、次は int 型の val 変数を宣言する。

let val: int;

変数は全て local になる。ただし、最上位のスコープに定義することで、そのモジュール内でグローバルなデータとなる。

最上位のスコープに定義する変数の let の前に pub を指定すると、外部のモジュールから参照可能な変数となる。

また、pub の代わりに global を宣言すると、VM 内でグローバルな変数となる。ただしグローバルに登録されるのは、この宣言を含むモジュールを import したタイミングとなる。

同名のグローバルシンボルが定義されている場合の動作は未定義とする。

同一スコープ内に、同名の変数を宣言することはできない。

mutable 制御

変数には mutable 制御が不可欠です。必ずこちらを参照してください。

https://qiita.com/dwarfJP/items/29540d0767d50cfce896

nilable の変数宣言

宣言する型に ! を付加することで nilable になります。

例えば次の val は、int の nilable 型となり、int と nil を設定可能であるのに対し、val2 は、 nil を設定できない変数となります。

非 nilable の変数に対して nil を代入すると、コンパイルエラーとなります。

let val: int! = 1;
let val2: int = nil; // error

nilable は nil となる可能性がありますが、非nilable の型は nil になりません。つまり、非 nilable 型を利用している間は、意図しないタイミングで nil アクセスエラーが発生しないことを保証できます。

nilable 型の値は、そのままでは本来の型としては使用できません。

次の例では、int! 型の val は int として演算に使用できず、コンパイルエラーとなります。

let val: int! = 1;
let val2 =  val + 1; // error

nilable 型から本来の値に戻すには、次のいずれかの syntax を利用します。

  • unwrap
  • unwrap!
  • let!
  • sync!
  • if!
  • if! let

nilable 関連の仕様

ここでは nilable 関連の仕様について説明します。

マップ型の値取得

map 型の要素にアクセスした場合、その結果は必ず nilable 型になります。

たとえば、次の map.B は int! となります。

let map = { "A": 10, "B": 11, "C": 12 };
let val = map.B; // int!

unwrap

unwrap は、直後に続く式の nilable から非 nilable 型に変換する式です。

unwrap exp [ default insexp ]

unwrap の評価結果は、 exp の nilable を外した型となります。

exp には、評価結果が nilable となる式を渡す必要があります。insexp には、 exp が nil だった時に、代わりとなる式を渡します。insexp の型は、 exp の nilable を外した型でなければなりません。例えば exp が int! だった場合、 insexp は int 型でなければなりません。default が省略されていて exp が nil だった場合、プログラムはエラー終了します。

exp が nilable でない場合は、 コンパイルエラーします。

{
  let val: int! = nil;
  let val2 = unwrap val default 0;
  print( "%d", val ); // 0
}
{
  let val: int! = 1;
  let val2 = unwrap val default 0;
  print( "%d", val ); // 1
}

上記の例は、最初の unwrap では val が nil のため default の評価結果が返り、2つめの unwrap では val が 1 のため、1 が返っている。

unwrap!

unwrap! は、 前述の unwrap 処理と、変数への代入を同時に行ないます。

unwrap! symbol {, symbol }  = exp[, exp ] block [then thenblock];

exp が nil でない場合、 unwrap の結果を symbol に代入します。

いずれかの exp が nil だった場合、ブロック block を実行します。このブロック内では次のいずれかの処理を行なう必要があります。

  • symbol に対して適切な値を設定する
  • symbol を定義しているスコープから抜ける。

もしも上記の処理を行なわない場合、その後の動作は未定義です。

またブロック block 内では、 _exp%d のシンボルで、exp の unwrap の結果にアクセスできます。%d は 1 から始まる数字で、 symbol の順番に対応します。

このブロック block 内では、symbol の値は未定義となります。

then ブロックは、 exp が全て nil でなかった場合に実行されます。このブロック内からは、symbol にアクセス出来ます。

fn test( arg:int! ) {
  let val = 0;

  unwrap! val = arg { print( 0 ); return; } then { val = val + 1; }
  print( val );
}
test( 1 );  // print( 2 );
test( 2 );  // print( 3 );
test( nil );  // print( 0 );

let!

let! は、変数宣言と unwrap を同時に行ないます。

let! symbol {, symbol } = exp[, exp ] block [ then thenblock ];

block と thenblock の扱いは unwrap! と同じです。適切な処理をしない場合、 symbol の値は未定義です。

block ブロック内では '_' + symbol の名前で exp の unwrap の結果を参照できます。

thenblock ブロック内では symbol で値を参照できる。

fn test( arg:int! ) {
  let! val = func() { print( 0 ); return; } do { val = val + 1; }
  print( val );
}
test( 1 );  // print( 2 );
test( 2 );  // print( 3 );
test( nil );  // print( 0 );

sync!

sync! は、 unwrap 処理を行ないます。

sync! symbol {, symbol } = exp[, exp ] block [then thenblock] do doblock;

exp と symbol, thenblock の扱いは unwrap! と、ほぼ同じです。異なるのは、 symbol のスコープが thenblock と doblock に限定されることです。

doblock は、 block と thenblock を処理した後に実行されるブロックです。

sync! は、doblock ブロック処理終了後に次の処理を行ないます。

  • sync! を使用したスコープに、symbol で宣言したシンボル名と同じシンボルがある場合、

doblock ブロック終了時点の symbol の値を反映する。

ただし、 doblock を return 等で抜けた場合は反映されない。

なお sync! で宣言した symbol から、上位スコープ内の同名の symbol へは、代入可能な関係でなければならない。

例えば次は、test() 関数内で sync! を実行している。この sync! は val に func() の結果を格納しており、doblock で val を変更している。doblock が終了すると、val の値が外側のスコープの val に反映される。

fn test( arg:int!, arg2:int! ) {
  let mut val = 1;
  let val2 = 1;
  sync! val, val3 = arg, arg2 { print( 0 ); return; } do { val = arg + arg2; }
  print( val );
}
test( nil );  // print( 0 );

if!

if! は、 unwrap 処理による条件分岐です。

if! exp block [ else elseblock ];

exp には nilable な式を指定します。exp が nil でなかった場合、 block を実行します。exp が nil だった場合、 elseblock を実行します。

block 内の処理では _exp で、 exp の unwrap の結果にアクセスできます。

if! let

if! let は、 unwrap 処理による条件分岐です。

if! let var[,var,...] = exp[,exp,...] block [ else elseblock ];

exp には nilable な式を指定します。exp が nil でなかった場合、 block を実行します。exp が nil だった場合、 elseblock を実行します。

block 内の処理では var で宣言した変数にアクセス出来ます。var の変数には exp の unwrap の結果が格納されます。

一般制御文

Lua と同じ制御文(if,while,for,repeat)をサポートする。

Lua と同様に、continue はない。

if

if exp {
}
elseif exp {
}
else {
}

if は Lua と同じ構文とする。ただし、ブロックは {} で宣言する。このブロックは必須である。C のようにブロックを宣言せずに 1 文だけ書くことはできない。

switch

switch exp {
  case condexp [, condexp] {
  }
  case condexp {
  }
  default {
  }
}

switch は、exp の結果と一致する condexp を探し、一致するブロックを実行する。どの condexp にも一致しない場合は default のブロックを実行する。condexp は , で区切って複数指定できる。複数指定した場合、いずれかと一致したブロックを実行する。

while, repeat

while exp {
}

repeat {
} exp;

while, repeat は Lua と同じ構文とする。ただし、ブロックは {} で宣言する。このブロックは必須である。C のようにブロックを宣言せずに 1 文だけ書くことはできない。

for

for name = exp1, exp2, exp3 {
}

for は、イテレータを使用しないタイプの制御とする。イテレータを利用するタイプは each とする。

ブロックは {} で宣言する。このブロックは必須である。C のようにブロックを宣言せずに 1 文だけ書くことはできない。

foreach

foreach val [, index ] in listObj {
}
foreach val [ , index ] in arrayObj {
}
foreach val [, key ] in mapObj {
}

foreach は、 List, Array, Map のオブジェクトが保持する要素に対して処理を行なう。

val には各オブジェクトが保持する要素が格納され、body が実行される。index には要素のインデックス、 key には要素を紐付けているキーが格納される。index, key は省略可能。

apply

apply val {,val2 } of exp {
}

apply は、イテレータを使用するタイプの for とする。ブロックは {} で宣言する。このブロックは必須である。C のようにブロックを宣言せずに 1 文だけ書くことはできない。

val には、イテレータで列挙された値が格納される。イテレータが複数の値を列挙する場合, その値を格納する val2 , val3… を宣言する。

exp の仕様は Lua の for と同じ。

goto

goto はサポートしない

関数宣言

[ pub | global ] fn name( arglist ) : retTypeList {
}

関数宣言は、上記のように fn で行ない、name で関数名を指定する。name は省略可能。引数は arglist で宣言し、変数宣言の let を省略した形で宣言する。戻り値の型は、retTypeList で宣言する。型宣言は 変数宣言の : 以降と同じ。関数は複数の値を返すことができる。 retTypeList は返す値の分の型を宣言する。

関数を外部モジュールに公開する場合は、fn の前に pub を宣言する。ただし公開可能な関数は、最上位のスコープで定義した関数でなければならない。例えば if や while 等のブロック内で定義した関数は、公開できない。

最上位のスコープに定義する関数において、pub の代わりに global を指定すると、VM 内でグローバルとなる。ただし登録されるのは、この宣言を含むモジュールを import したタイミングとなる。

同名のグローバルシンボルが定義されている場合の動作は 未定義 とする。

関数宣言に関して、次の制限を持つ。
- 関数オーバーロードをサポートしない
- 演算子オーバーロードをサポートしない

fn plus( val1: int, val2: int ) : int {
  return val1 + val2;
}
fn plus1( val1: int, val2: int ) : int, int {
  return val1 + 1, val2 + 1;
}

可変長引数

可変長引数は Lua の … を利用する。

なお、 … の各値は stem! 型として扱う。

fn hoge( ... ) : stem! {
  let val: stem! = ...;
  return val;
}

例えば、上記関数は引数に与えらえた第一引数を return するが、このときの型は stem! となる。

関数コール

関数コールは 関数オブジェクト()で行う。

クラス宣言

オブジェクト指向プログラミングのためのクラスをサポートする。

クラスに関して、次の制約を持つ。
- 多重継承はサポートしない。
- generics(template) はサポートしない。
- 全てがオーバーライド可能なメソッドとなる。
- オーバーライドの抑制はできない。
- 継承間で引数の異なる同名メソッドは定義できない。
- ただし、コンストラクタは例外で同じ名前( __init )。

クラス宣言の最小サンプルを示す。

class Hoge {
}

このサンプルは、 Hoge という名前のクラスを宣言している。メンバもメソッドも持たないため、現実的に利用することはないだろうが、クラス宣言としてはこれが最小である。

なお、 class を外部モジュールに公開する場合は、次のように pub を付けて宣言する。

pub class Hoge {
}

メンバ、メソッド

クラスはメンバ(変数)、メソッド(関数)を持つことが出来る。

例えば、次は val1,val2 のメンバと、 func() のメソッドを持つ。

class Hoge {
  let val1:int;
  let val2:int;
  pub fn func( val:int ): int {
     return val + self.val1 + self.val2;
  }
}

メソッドの処理から自分自身のインスタンスにアクセスする場合、self を利用する (C++ の場合 this )。

なお、 C++ ではメソッドの処理から自分自身のメンバやメソッドにアクセスする場合、次のように this ポインタを経由する方法と、そのまま直にアクセスすることが可能である。

this->val = 1;
val = 1;

一方で、LuneScript では必ず self を使用しなければならない。

アクセス制御

LuneScript では、メンバ、メソッドのアクセス制御を行なえる。

アクセス制御には 'pub', 'pro', 'pri' を指定する。

それぞれの意味は次の通り。 (C++ と同じ)

  • pub
    • どこからでもアクセス可
  • pro
    • サブクラスからアクセス可
  • pri

    • このクラス内からのみアクセス可

アクセス制御を明示しない場合、デフォルトの pri が使用される。

次の例は、 val1 が pri, val2 が pro, func が pub である。

class Hoge {
  pri let val1:int;
  pro let val2:int;
  pub fn func( val:int ): int {
     return val + self.val1 + self.val2;
  }
}

abstract

メソッドは、実態を持たずに型だけ宣言することが出来る。

abstract class Hoge {
  pub abstract fn func( val:int ): int;
}

実態を持たないメソッドは abstract として宣言する必要がある。

abstract メソッドを持つクラスは abstract クラスとして宣言する必要がある。

abstract クラスは、そのクラス単独では new できない。必ず継承する必要がある。

メソッドの外部定義

メソッドは、クラス定義の外部で定義することが出来る。

ただし、同一ファイルで定義する必要がある。

たとえば次のクラスは、

class Hoge {
  fn func() {
  }
}

次のようにも定義できる。

class Hoge {
}
fn Hoge.func() {
}

また、次のようにも宣言できる。

class Hoge {
  fn func();
}
fn Hoge.func() {
}

インスタンスの生成

クラスのインスタンス生成には new を使用する。

次は、 Hoge クラスのインスタンスを生成している。

class Hoge {
}
let hoge = new Hoge();

new 演算子の後には、クラスを指定する。クラスがメンバを持つ場合、次のように設定するメンバの値を new の後のクラスの () で指定する。

class Hoge {
  let val1:int;
  let val2:int;
}
let hoge = new Hoge(1,2);

コンストラクタ

クラスはコンストラクタを持てる。コンストラクタは、クラスの全メンバの初期化を行なう。

例えば次の場合、 コンストラクタで val1, val2 の初期化を行なっている。

class Hoge {
  let val1:int;
  let val2:int;
  pub fn __init() {
    self.val1 = 0;
    self.val2 = 0;
  }
}
let hoge = new Hoge();

このとき、new に続くクラス名の後に指定する引数には値を指定しない。new の引数はそのクラスの引数であり、この例のクラスのコンストラクタは引数を持たないため new には値を指定しない。

なお、コンストラクタを自前で作成しない場合は、自動で全メンバを引数に持つコンストラクタが生成される。この時生成されるコンストラクタの引数は、メンバの宣言順となる。

コンストラクタを自前で作成する場合、次の制約がある。

  • 全てのメンバを初期化しなければならない。
  • コンストラクタの宣言の後に、メンバを宣言してはならない。
  • return を使用してはならない。

スーパークラスのコンストラクタをコールする場合は super() を使用する。super() は、コンストラクタの先頭で呼び出す必要がある。

クラスを継承している場合、コンストラクタは自前で作成しなければならない。

static

メンバ、メソッドの宣言時 static を付加することで、静的なメンバ、メソッドを作成することが出来る。

次は、static なメンバ val, メソッド func() を持つクラスのサンプルである。

class Hoge {
  static let val:int;
  __init {
    Hoge.val = 1;
  }
  pub static fn func():int {
     return 2;
  }
}
print( Hoge.val, Hoge.func() ); // 1, 2

static メンバ、メソッドは、インスタンスを生成せずに利用できる。

__init ブロック

static なメンバを初期化するブロックである。

static なメンバを持つクラスは、必ず __init ブロックを宣言しなければならない。

__init ブロックは次の制約がある。

  • 全ての static メンバを初期化しなければならない。
  • __init ブロックの後に、 static メンバを宣言してはならない。

アクセッサ

メンバ宣言時に、アクセッサを同時に宣言できる。

このアクセッサは getter, setter の順に宣言し、宣言箇所にはアクセス権限(pub/pro/pri)を指定する。

例えば次の場合、メンバ val に対して pub の getter と pri の setter が作られる。

let pri val : int { pub, pri };

作られる getter と setter は、 get_val(), set_val() のメソッドとなる。同名のメソッドが存在する場合は、この宣言は無視される。

アクセッサ宣言の {} を省略した場合、アクセッサは作成されない。getter だけ指定し、 setter を省略した場合は、 getter だけ作成される。

getter アクセス

メンバの getter にアクセスする際は、.get_member() だけでなく、 .$member でもアクセスできる。

なおアクセッサではなく、メンバ member 自体が pub だった場合も.$member でアクセスできる。

class Test {
  pri val: int { pub };
}
Test test = new Test( 10 );
print( test.$val );  -- 10

advertise

LuneScript は、メンバのメソッドを自分のメソッドとして透過的に利用することが出来る。

次の例で説明する。

class Hoge {
   pub fn func() {
      print( "Hoge.func()" );
   }
}
class Foo {
   pri let hoge:Hoge;
   pub fn __init() {
      self.hoge = new Hoge();
   }
   advertise hoge;
}
let foo = new Foo();
foo.func(); // Hoge.func()

上の例では、クラス Foo はメンバに Hoge クラスの hoge を持つ。そしてクラス Foo は、メンバ hoge を advertise している。これによって、クラス Foo は Hoge クラスのメソッド func() を持つことになり、foo.func() を実行すると、内部的に Foo.hoge.func() が実行される。

なお advertise は、advertise しているクラスに同名のメソッドがある場合、そちらのメソッドを優先する。

例えば次の例では、クラス Hoge はメソッド func1(), func2() を持ち、クラス Foo はメソッド func1() を持つ。この場合、クラス Foo のメソッド func1() が優先される。

class Hoge {
   pub fn func1() {
      print( "Hoge.func1()" );
   }
   pub fn func2() {
      print( "Hoge.func2()" );
   }
}
class Foo {
   pri let hoge:Hoge;
   pub fn __init() {
      self.hoge = new Hoge();
   }
   pub fn func1() {
      print( "Foo.func1()" );
   }
   advertise hoge;
}
let foo = new Foo();
foo.func1(); // Foo.func()
foo.func2(); // Hoge.func()

継承

LuneScript は、クラスの継承をサポートする。ただし、多重継承はサポートしない。

その代わりに、インタフェースをサポートする。

継承は次のように extend で宣言する。

class Super {
}
class Sub extend Super {
  pub fn __init() {
     super();
  }
}

この例は、Sub クラスが Super クラスを継承している。

override

全てのメソッドはオーバーライド可能である。

メソッドをオーバーライドする場合、次のように override を宣言しなければならない。

class Super {
  pub fn func() {
  }
}
class Sub extend Super {
  pub fn __init() {
     super();
  }
  pub override fn func() {
  }
}

インタフェース

インタフェースは、メソッドの型だけを宣言可能なクラスである。

メンバを持つことや、メソッドの処理を定義することは出来ない。

次の例は、クラス Test で インタフェース IF をインプリメントしている。

interface IF {
  pub fn func();
}
class Test extend (IF) {
  pub fn func() {
     print( "Test.func" );
  }
}
fn sub( obj:IF ) {
  obj.func();
}
sub( new Test() );

メソッド 呼び出し

メソッド呼び出しは、次のように行なう。

Hoge hoge;
Hoge.sub();
hoge.func();

Hoge.sub() はクラスメソッドで、hoge.func() はインスタンスメソッドである。

クラスメソッドは クラスシンボル.メソッド()
メソッドは インスタンス.メソッド() で呼び出す。

Lua のような ':' と '.' の使い分けではなく、どちらも '.' を利用する。

プロトタイプ宣言

LuneScript は、スクリプトの上から順に解析する。

スクリプトで参照するシンボルは、事前に定義されている必要がある。例えばクラス TEST 型の変数を宣言するには、事前にクラス TEST を定義する必要がある。

また、交互に参照するクラスを定義するには、どちらかをプロトタイプ宣言する必要がある。

次は、 ClassA, ClassB がそれぞれを参照する時の例である。

class Super {
}
pub proto class ClassB extend Super;
class ClassA {
  let val: ClassB;
}
pub class ClassB extend Super{
  let val: ClassA;
}

proto は上記のように宣言する。

プロトタイプ宣言と実際の定義において、pub や extend など同じものを宣言しなければならない。

マクロ

LuneScript は簡易的なマクロを採用する。

Lisp などのような本来のマクロではなく、あくまでも簡易的な機能である。

マクロは次のように定義する。

macro _name ( decl-arg-list ) {
  { macro-statement }
  expand-statement
}

マクロ定義は、予約語 macro で始める。続いてマクロ名 _name を指定する。マクロ名は _ で始まらなければならない。

decl-arg-list は、マクロで使用する引数を宣言する。マクロの引数は、 プリミティブな値 でなければならない。

macro-statement は、 expand-statement で使用する変数を設定する処理を書く。expand-statement で書いた内容が、マクロで展開される。

次は、単純なマクロの例である。

macro _hello( word: str ) {
  print( "hello" .. str ); 
}
_hello( "world" ); // print( "hello" .. "world" );

この例では macro-statement は無く、 expand-statement だけがあり、expand-statement の print が展開されている。

マクロ内では、他の関数と同じように処理を書ける。ただし、 macro-statement 内では、標準関数の一部しか利用できない。

C のような定数に名前を付けるためにマクロは利用できない。そのような使い方をしたい場合は enum を使用すること。

マクロ内で利用できる追加 syntax

マクロ内では、次の特殊な syntax を追加で利用できる。

  • ,,,,
  • ,,,
  • ,,
  • `{ }

,,,, は、直後に続く式を評価して得られた シンボル文字列に変換 する演算子である。,,, は、直後に続く式を評価して得られた 文字列シンボルに変換 する演算子である。

`{} は、 `{} 内で書いたステートメントを、そのままの値とすることが出来る。macro-statement 内で `{} で書いたステートメントは、expand-statement で展開することができる。`{} 内では変数の参照や関数の実行を書いても、macro-statement 内では評価されない。expand-statement で展開時に評価される。

,, は、直後に続く を評価する演算子である。,,、 ,,,、 ,,,,、 は、 macro-statement の `{} 内で利用することで、式を評価することが出来る。

macro-statement で ,, を利用すると、直後の式を評価するが、expand-statement では、式の評価ではなく、変数の展開に限定される。

例えば次のマクロでは、

macro _test2( val:int, funcxx:sym ) {
    {
        fn func(val2:int):str {
            return "mfunc%d" (val2);
        }
        let message = "hello %d %s" ( val, ,,,,funcxx );
        let stat = `{ print( "macro stat" ); };
        let stat2 = `{
            for index = 1, 10 {
                print( "hoge %d" ( index ) );
            }
        };
        let mut stat3:stat[] = [];
        for index = 1, 4 {
            stat3.insert( `{ print( "foo %d" ( ,,index ) ); } );
        }
        let stat4 = ,,,func( 1 );
    }
    print( ,,message );
    ,,funcxx( "macro test2" );
    ,,stat;
    ,,stat2;
    ,,stat3;
    ,,stat4( 10 );
}
fn mfunc1( val: int ) {
    print( "mfunc1", val );
}

_test2( 1, print );

マクロ展開によって次のように展開される。

print( "hello 1 print" );                       // print( ,,message );
print( "macro test2" );                         // ,,funcxx( "macro test2" );
print( "macro stat" );                          // ,,stat
for index = 1, 10 {                             // ,,stat2
  print( "hoge %d" ( index ) );
}
print( "foo %d" ( 1 ) );                        // ,,stat3
print( "foo %d" ( 2 ) );
print( "foo %d" ( 3 ) );
print( "foo %d" ( 4 ) );
mfunc1( 10 );                                   // ,,stat4( 10 );

ここで注目すべき点は、次の点である。

  • _test2( 1, print ) のマクロ呼び出しで print を渡しているが、これは print が保持する関数オブジェクトを渡しているのではなく、print シンボルそのものを渡している。
  • stat2 は、 for 文そのものを展開しているのに対し、stat3 は、 for 文で作成したステートメントリストを展開している。

上記の通り、マクロ内では通常の型以外に次の型を利用できる。

  • シンボルを格納する sym 型
  • ステートメントを格納する stat 型

マクロはステートメントを定義する箇所であれば、どこでも呼び出せる。マクロ内でクラスや関数を定義することもできる。

macro-statement 内で利用可能な関数

macro-statement 内では、 次の関数が利用できる。

  • 標準関数
  • _lnsLoad()

ここで _lnsLoad() 関数は次の型である。

fn _lnsLoad( name:str, txt:str ): stem;

この関数は、指定の txt の lunescript コードをロードし、ロード結果を返すものである。

例えば次のサンプルでは、

macro _test( txt:str ) {
   {
      let val = _lnsLoad( "aa", txt )$.val;
   }
   print( ,,val, type( ,,val ) );   // 100, number
}

_test( ```
pub let val = 100;
``` );

_test() マクロに pub let val = 100; を与えており、これを _lnsLoad() でロードすることで、その val に 100 が格納されたモジュールが返される。そして、そのモジュールの val を、変数 val に代入し、expand-statement で print() している。

なお、 _lnsLoad() の第一引数の name は、ロードする lunescript コードの名前を指定する。

マクロの意義

マクロは通常の関数と比べて幾つかの制限がある。またマクロで行なえる処理は、関数等を組合せることで実現できる。

では、マクロを使う意義は何か?

それは、「マクロを使うことで静的に動作が確定する」ことである。

同じ処理を関数で実現した場合、動的な処理となってしまう。一方、マクロで実現すれば、静的な処理となる。

これの何が嬉しいのか?

それは、静的型付け言語が動的型付け言語よりも優れている点と同じである。

静的に決まる情報を静的に処理することで、静的に解析できる。

例えば、オブジェクト指向の関数オーバーライドの大部分は、マクロを利用することで静的に解決することができる。動的な関数オーバーライドではなく、静的な関数呼び出しにすることで、ソースコードを追い易くなる。

無闇にマクロを多用するは良くないが、安易に関数オーバーライドなどの動的処理にするのも理想ではない。

動的処理とマクロは適宜使い訳が必要である。

モジュール

LuneScript は、 1 ファイル 1 モジュールとなる。モジュールは、それぞれ名前空間が異なる。

例えば lune/base/Parser.lns は、lune.base.Parser の名前空間となる。

スクリプトファイル内で pub 宣言された関数、クラスは、外部モジュールからアクセス可能となる。

import

外部モジュールを利用する際に import 宣言する。

import はスクリプトの最上位スコープで宣言しなければならない。

import hoge.foo.module1;

上記は、サーチパスから hoge/foo/module1.lns を検索し、利用可能とする。

module1 のクラス、関数にアクセスするにはmodule1.class, module1.func のようにアクセスする。

インポートしたシンボル(上記の場合は module1 )を変数として扱うことは出来ない。

モジュールは、相互参照出来ない。

例えば ModuleA, ModuleB があったとき、ModuleA から ModuleB を import,ModuleB から ModuleA を import することは出来ない。

require

Lua の外部モジュールを利用する際に宣言する。

let mod: stem! = require( 'module' );

require の結果は stem! 型となる。

モジュールは、相互参照出来ない。

_lune.lua モジュール

前述している通り LuneScript で Lua へトランスコンパイルしたファイルは、Lua コマンドでそのまま実行できます。この時、外部モジュールを必要としません。

これは、トランスコンパイルした Lua コード内に、処理に必要なコードを全て含めていることを示します。

例えば次の処理コードをトランスコンパイルすると、

fn func( val:int! ):int {
   return 1 + unwrap val default 0;
}

Lua コードは次のようにだいぶ長くなります。

 1  --mini.lns
 2  local _moduleObj = {}
 3  local __mod__ = 'mini'
 4  if not _ENV._lune then
 5     _lune = {}
 6  end
 7  function _lune.unwrap( val )
 8     if val == nil then
 9        __luneScript:error( 'unwrap val is nil' )
10     end
11     return val
12  end 
13  function _lune.unwrapDefault( val, defval )
14     if val == nil then
15        return defval
16     end
17     return val
18  end
19  
20  local function func( val )
21     return 1 + _lune.unwrapDefault( val, 0)
22  end
23  
24  return _moduleObj

この 4 〜 18 行目が unwrap に必要な処理となります。なお、このコードは全ての Lua ファイルに出力されます。

このコード自体は共通処理であるため、トランスコンパイルする際に -r オプションを指定することで、別モジュールとして require して共通処理をまとめることができます。

具体的には次のように -r オプションを指定します。

$ lua lune/base/base.lua -r src.lns save

この -r オプションを指定した場合、上記のコードは次のように変換され、かなりスッキリします。

--mini.lns
local _moduleObj = {}
local __mod__ = 'mini'
_lune = require( "lune.base._lune" )
local function func( val )

   return 1 + _lune.unwrapDefault( val, 0)
end

return _moduleObj

なお、require( "lune.base._lune" ) が挿入されるため、このモジュールがロード出来るようにセットしておく必要があります。トランスコンパイラが動作する環境であれば意識する必要はありませんが、変換後の Lua ソースをどこか別の環境で実行するような場合は注意が必要です。

emacs 対応

LuneScript 編集用の emacs のメジャーモード lns-mode.el を用意しています。

https://github.com/ifritJP/LuneScript

emacs ユーザはご利用ください。

セルフホスティング

LuneScript のトランスコンパイラは、極一部を除いて LuneScript で開発しています。

具体的には、LuneScript のソースコードサイズ 約 385KB 中、99.99% は LuneScript で開発しています。 残りの 0.01% は Lua です。

セルフホスティングで開発することで、次の利点があります。

  • それなりの規模のスクリプトでの、使用実績が出来る。
  • テストのためだけのスクリプトの作成を、最小限に出来る。
  • その言語を使い倒すことになるため、その言語の長所・短所が実感出来る。
  • 短所を早期発見できるので、すぐに改善策を検討できる。

もし、今後自分で言語を設計・開発しようと考えている方がいれば、セルフホスティングで開発することをオススメします。