Lua のトランスコンパイラを考える (LuneScript)

Table of Contents

ここの情報は、古くなっています。 新しい情報は次の URL を参照してください。

https://github.com/ifritJP/LuneScript

lctags の機能追加が一段落したので、 別ツールの開発に取り組もうと思う。

その別ツールとして検討しているのが、Lua のトランスコンパイラである。

ここでは、検討中の Lua のトランスコンパイラについて内容をまとめる。

Lua のトランスコンパイラの仕様については、検討した結果を随時更新する。

1 Lua のトランスコンパイラが必要な理由

Lua は軽量で、かつ実行パフォーマンスも高い言語である。 しかし、Lua には次に挙げる欠点がある。

  • Lua は動的型付け言語であるため、動的型付け言語の欠点が全て当てはまる。
  • Lua は C や Java などの言語に比べるとマイナーな言語であり、開発をサポートするツールが少ない。

規模の小さいスクリプトを作成している分には問題ないが、 ある程度の規模のスクリプトを開発する場合、 上記問題はインパクトが大きい。

この問題を解決するために Lua のトランスコンパイラが必要となる。

ちなみに、ここで言う Lua のトランスコンパイラとは、 ある言語で書いたスクリプトを Lua のスクリプトへ変換するツールを指す。

1.1 欠点に対するアプローチ

ここでは、Lua の上記欠点に対するトランスコンパイラを用いたアプローチを示す。

  • 動的型付け言語の欠点
    • トランスコンパイラの変換元の言語に静的片付け言語を採用することで、 動的型付け言語の欠点をカバーすることが出来る。
    • これは typescript と javascript の関係と同様
  • 開発をサポートするツールが少ない
    • トランスコンパイラの変換先の言語に Lua だけでなく、 C 等のメジャーな言語をサポートすることで、 そのメジャーなサポートツールの利用が可能となる

2 トランスコンパイラに必要な要件

上記の欠点をカバーするためのトランスコンパイラに必要な要件を挙げる。

  • 変換元の言語として、静的片付け言語を採用する
  • 変換先の言語として、 Lua と C/C++ 言語をサポートする。
    • 以降 C と記載した場合は C++ も含める。
    • C への変換は、C でビルドして native で動かすのが目的ではなく、 メジャーな言語に変換してサポートツールを利用するのが目的である。
      • よって、変換後の C プログラムの実行パフォーマンスを優先しない。
      • luaSocket 等の標準外モジュールを利用したプログラムは、 C への変換を行なっても、変換後のリンクまではサポートしない。
        • この場合、プログラムの動的な情報を元に開発をサポートするツールが 利用できなくなってしまうが、 開発をサポートするツールは動的な情報と静的な情報を利用するタイプがあり、 静的な情報を利用するタイプはリンクまで出来なくても利用出来る。
      • よって、標準外モジュールの C へのコード変換は非サポートとする。
      • ただし、標準外モジュールのスタブの雛形までは作成する。
        • スタブを作成し、スタブの中身を実装すれば動かせるレベルにする。
    • Lua への変換は、変換後の Lua スクリプトの実行パフォーマンスを優先する。
      • というか、変換元の言語ほぼそのまま Lua に落せるような Syntax にする。
      • 変換時に最適化のようなことはせず、基本的には書かれたままの処理にする。
  • トランスコンパイラ自体を Lua で実行可能なスクリプトとして開発する
  • 変換元の言語で書かれたスクリプトを読み込み、そのまま実行可能とする
    • 実行時に Lua ファイルと中間ファイルを生成し、次に実行する際は変換済みファイルを利用する
    • 中間ファイルには、元のファイルに定義されている型やメソッドなどのメタ情報を含める

以上の要件を満す変換元の言語として利用可能な既存の言語は無い。 ちなみに、 Lua へトランスコンパイル可能な言語として既に Moon Script があるが、 Moon Script の Syntax から C への変換は困難だと感じた。

もしかしたら他に変換元言語として相応わしい言語があるかもしれないが、 少なくとも私は知らないため、ここでは新しく言語を作成することを考える。

まぁ「いつかは実用的な新しい言語を作ってみたい」という 技術的好奇心が大きいことは否定しない。

3 変換元の言語の要件

変換元言語の名称は、LuneScript (ルーンスクリプト)とする。

LuneScript の要件を挙げる

  • 変換先の言語に C 言語をサポートするため、 メモリ管理として gc を前提にしない。
    • とはいえ、alloc/free を明示するのはイマドキ有り得ないので、 Rust の所有権方式を参考にする
  • 学習コストを下げるため、Syntax は C 言語/Lua を基調とする。
  • コルーチンや anonymous 関数、クロージャ等、 Lua が採用している機能をなるべく採用する。
    • ただし、 metatable の概念は Lua の独自色が強過ぎするので採用しない。
  • 言語レベルでオブジェクト思考プログラミングをサポートする
  • Lua 単体で実現出来ない機能は、採用しない。
    • Lua の拡張モジュールの利用を前提としない。
    • 変換後の Lua ソースの可読性が多少悪くなっても、 Lua 単体で実現可能であれば採用を検討する。
    • 前述の通り、変換後の C ソースはビルドしてオブジェクトが出来ることは保証するが、 リンクまでは保証しない。
      • もちろん変換後の C ソースは、変換前のソースと論理的に一致させる。
  • 値は符号付き整数(int)と浮動小数点実数(real)をサポートする。
    • ビット幅の違いや、符号の有無はサポートしない。
  • Lua スクリプトで書かれた外部モジュールは、 Glue 無しで LuneScript からそのまま利用可能とする。
  • Lua の標準関数を全て利用可能とする。
    • ただし、関数名は完全一致しなくても良い。
  • LuneScript を使って lctags を開発するのに困らないレベルにする。
  • LuneScript 内に記載したコメントは、変換前の位置に該当する変換後の位置にそのまま挿入する。
    • これは lint 等の静的解析ツールで指摘された際、 その指摘を抑制するためにコメントが利用されるため、 コメントが所定の位置に挿入されることが必要。
  • Lua の table の概念を、array(list)と map に分ける。
    • これは出力先に C を考えたときに array と map に分けた方が扱い易いのと、 そもそも Lua の table が ipairs と pairs で動きが変わる設計なのがイマイチなので。
    • ただし、 array のインデックスは 1 からとする。
    • array, map は generics をサポートする。

4 LuneScript の開発方針

  • 前述の通り、 LuneScript のトランスコンパイラは Lua で動作可能とする。
  • また、トランスコンパイラ自体を LuneScript で開発する。
    • トランスコンパイラを開発するのに最低限必要な部分を Lua で作成し、 LuneScript から C への変換に必要なライフタイムチェックなどの複雑な処理を含めて LuneScript で作成する。
  • トランスコンパイラ自体を LuneScript で開発することで、 実用に耐える品質を担保する。

5 LuneScript syntax

ここでは LuneScript の Syntax を示す。

5.1 組込み型

組込み型として、次の型を持つ。

  • int
    • 符号付き整数
  • real
    • 浮動少数点実数
  • enum
    • enum 型
  • str
    • 文字列 (Lua の文字列そのもの)
  • Array
    • 配列( インデックスは 1 から。 固定長。 )
  • List
    • リスト( インデックスは 1 から。 可変長。 Lua のシーケンスそのもの。 )
  • Map
    • キーと値の関連付け (Lua のテーブルそのもの。 )
  • func
    • 関数
  • stem
    • 上記のいずれか何でもあり
    • Lua の変数そのもの

5.2 数値リテラル

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

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

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

let val : int = ?a;  '' 0x61

上記のように ? に続く文字を ACSII コードに展開する。

5.2.1 演算

数値の演算は Lua と同じものを採用する。

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

ただし、 int と int の演算結果が int の範囲外になった場合、 値としては real になるが、LuneScript 上の型は int のままである。 C に変換後は、計算結果の型は int で、値も当然 int に丸められる。 Lua に変換後の演算結果を int に強制する場合は @int すること。

stem 型のデータは、そのままでは演算できないので、 次のように @int や @real で型変換後に演算する。

fn add1( val: stem ) : int {
  return val@int + 1;
}

5.3 文字列

文字列は " で囲む。 複数行コメントは """ で囲む。

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

let str : Str = "1234";
str[ 2 ] '' ?2

また、Python に似た format 書式を採用する。

"""
ここから〜
ここまで文字列"""
"10 + %s = %d" ("1", 11) '' "10 + 1 = 11"

5.3.1 文字列連結

文字列連結は Lua と同じ .. とする。

5.4 enum

int, string 型のプリミティブな値を、 enum 型として宣言できる。

enum VAL {
  val1,     '' 1
  val2,     '' 2
  val4 = 4, '' 4
  val5,     '' 5
}

enum VALS {
  vals1 = "1",
  vals1 = "2",
  vals1 = "3",
  vals1 = "4",
}

一つの enum 型のデータに int, string を混在させてはならない。 設定する値を指定しない場合は int 型となり、値は 1 から順に振られる。

let val = VAL.val1;
print( val ); '' 1
print( val.name ); '' "val1"
print( val.disp ); '' "val1(1)"
  • enum 型で宣言した値にアクセスするには、 "型名.シンボル" でアクセスする。
    • 上記の例で VAL.val1 にあたる。
    • val の型は VAL となる。
  • enum 型の値をそのまま評価すると、値に設定された値が返る。
    • 上記の例で val は 1 となる。
  • enum 型の値の name にアクセスすると、その値のシンボル名が返る。
    • 上記の例で val.name は "val1" となる。
  • enum 型の値の disp にアクセスすると、その値のシンボル名+値の文字列が返る。
    • 上記の例で val.disp は "val1(1)" となる。

5.4.1 型変換

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

変換する場合は次の書式を利用する。

val@type

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

val@int

例えば、上記は val の値を int に変換している。

  1. 数値型変換

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

    • int から real
      • 整数から実数に変換
    • real から int
      • 実数から整数に変換
      • math.ceil() を呼ぶのと等価。
  2. enum 型変換
    1. enum 型からの変換

      int 型のデータを保持する enum 型は、 暗黙的に int 型への変換が可能。 str 型のデータを保持する enum 型は、 暗黙的に str 型への変換が可能。

    2. enum 型への変換

      int 型、あるいは str 型から enum 型へ変換するには、new を使用する。

      enum VAL {
        val1,     '' 1
        val2,     '' 2
        val4 = 4, '' 4
        val5,     '' 5
      }
      let val: VAL! = new VAL( 1 );
      print( val.name ); '' "val1"
      
  3. stem 型との型変換

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

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

5.5 コメント

一行コメント ''、 複数行コメント ''' を指定可能。

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

5.6 演算子

原則的に、演算子 は Lua と同じものを利用する。 ただし、 Lua のメソッド呼び出しで利用する : は使用しない。

5.7 変数宣言

[pub|global] let name : type;

変数宣言は let で行なう。

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

ただし、変数宣言と代入を同時に行なう場合は型宣言を省略できる。

let val: int;

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

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

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

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

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

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

5.7.1 ! 型 (nilable)

変数の型の末尾に ! を付加することで、nil を保持可能な型(nilable)となる。 逆に言えば、 nilable でなければ、nil は保持出来ない。 ただし、 stem 型は nil を含めた全ての型のデータを保持できる。

例えば次の val は、nil と int を設定可能であるのに対し、 val2 は、 nil を設定できない。 これはコンパイル時に判定される。

let val: int!;
val = 1;
val = val + 1; '' error
let val2: int = nil; '' error

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

nilable 型の値は、そのままでは本来の型としては使用できない。 上記の例では val は int として演算に使用できない。

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

  • unwrap
  • unwrap!
  • let!
  1. unwrap

    nilable 型から本来の型にするには、 unwrap を使用する。

    unwrap は、式である。

    unwrap exp [ default insexp ]
    

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

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

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

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

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

  2. unwrap!

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

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

    exp が nil でない場合、評価結果を symbol に代入する。

    いずれかの exp が nil だった場合、ブロック block を実行する。 このとき、nil だった exp に対する symbol の値は未定義となる。 このブロック内では、 symbol に対して適切な値を設定するか、 symbol のスコープから抜けなければならない。 またブロック内では、 _exp%d のシンボルで、 exp の結果にアクセスできる。

    syncblock は、 symbol に値をセットした後に処理するブロックである。

    syncblock 処理終了後、syncblock 内で symbol に対して設定した値が、outsymbol に反映される。 ただし、syncblock を return 等で抜けた場合は反映されない。

    このとき symbol と outsymbol は、同じ型でなければならない。

  3. let!

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

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

    いずれかの exp の値が nil の場合、ブロックを実行する。 ブロックの処理で、この let を宣言したスコープから抜けるか、 symbol に適切な値を設定しなければならない。

    ブロック内では '_' + symbol の名前で exp の値を参照できる。

5.7.2 所有権とライフタイム

LuneScript は値の生存期間を所有権とライフタイムで管理する。 所有権とライフタイムは Rust を参考にする。

let owner: int;
let mut borrow: int;
let & ref: int;

次の値は、所有権が移動せずにコピーされる。

  • 数値型
  • func

次の値は、所有権の移動となる。

  • str
  • Array
  • List
  • Map
  • stem

5.7.3 代入

変数への代入は、 Lua と同じで右辺を評価後に代入を行なう。

左辺の変数の数と、右辺の値の数が異なる場合、エラーとする。 ただし、右辺の可変長の値を返す関数がある場合は、エラーとしない。

5.7.4 配列(Array)型の宣言

let name : type[@];

配列型は、上記のように型の後に [@] で宣言する。

let val: int[@];

例えば、上記は int 配列型の val 変数を宣言する。

  1. 配列型(Array)のコンストラクタ

    配列型のデータは、次のよう書くことで生成できる。

    [@ 1, 2, 3, 4, 5 ] '' int[@]
    [@ 0 : 5 ] '' [@ 0 0 0 0 0 ]
    

    ここで、 [@ 0 : 5 ] は、 値 0 を 5 個もつ配列を生成する。

    配列型のデータアクセスは Lua と同じ。

5.7.5 リスト(List)型の宣言

let name : type[];

リスト型は、上記のように型の後に [] で宣言する。

let val: int[];

例えば、上記は int を要素に持つリスト型の val 変数を宣言する。

  1. リスト(List)のコンストラクタ

    リスト型のデータは、次のよう書くことで生成できる。

    [ 1, 2, 3, 4, 5 ] '' int[]
    

    配列型のデータアクセスは Lua と同じ。

5.7.6 Map 型の宣言

let name : Map<keyType,valType>;

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

let val : Map<int,str>;

例えば、上記はキーが int 型で、値が str 型の変数 val を宣言する。

Map 型のデータアクセスは Lua と同じ。

  1. Map 型のコンストラクタ

    Map 型のデータは、次のように指定する。

    {  "a": 1, "b": 2, "c": 3, "d": 4, "e": 5 } '' Map<str,int>
    

5.8 制御文

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

Lua と同様に、continue はない。

5.8.1 if

if exp {
}
elseif exp {
}
else {
}

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

5.8.2 switch

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

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

5.8.3 while, repeat

while exp {
}

repeat {
} exp;

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

5.8.4 for

for name = exp1, exp2, exp3 {
}

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

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

5.8.5 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 は省略可能。

5.8.6 apply

apply val {,val2 } of exp {
}

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

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

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

5.8.7 goto

goto はサポートしない

5.9 関数宣言

[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;
}

可変長の値を返す関数は宣言できない。

ただし、table.unpack() は利用可能。

5.9.1 デフォルト値

実引数が省略された場合のデフォルト値を指定できる。

fn func( val1: int, val2: int = 1 ) : int {
  return val1 + val2;
}

5.9.2 可変長引数

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

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

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

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

可変長引数には、 Reference 型の値しか渡せない。

5.9.3 form

form によって、関数の型を定義する。

[pub] form name ( arglist ) : retTypeList;

例えば、次の宣言は引数と戻り値に int を持つ関数の型を add として定義している。

form add( val: int ) : int;

この form を利用することで、引数として与える関数型を指定することができる。

fn sub( func: &add ): int {
  return func( 0 );
}

例えば上記の関数 sub は、引数に add 型の関数型を引数に持ち、 その関数をコールしている。

5.9.4 関数コール

関数コールは Lua と同じ。

ただし、可変長引数の場合を除いて、 コールする関数の仮引数と実引数の数は等しくなければならない。

5.9.5 クロージャ

クロージャの動作は Lua と同じ。

ただし、所有権の概念が導入される。

  1. @@ 演算子

    @@ 演算子は、クロージャを簡易的に作成する。

    たとえば次の例は、 func 関数の第 1 引数に 10 を与えたクロージャと、 func 関数の第 1 引数に 10, 第 2 引数に 20 を与えたクロージャを作成する。

    fn add( val1: int, val2 ): int {
      return val1 + val2;
    }
    let add10 = add@@10;
    add10( 20 ); '' 10 + 20
    let add10_20 = add@@10,20;
    add10_20(); '' 10 + 20
    
  2. @@? 演算子

    @@ 演算子は、適応する引数が対象関数の第一引数に固定だったが、 @@? 演算子は、適応する引数を指定できる。

    たとえば次の例は、 func 関数の val2 に 10 を与えたクロージャと、 func 関数の val1 に 10, val2 に 20 を与えたクロージャを作成する。

    fn sub( val1: int, val2 ): int {
      return val1 - val2;
    }
    let sub10 = sub@@?val2=10;
    sub10( 20 ); '' 20 - 10
    let sub10_20 = sub@@?val1=10,val2=20;
    sub10( ); '' 10 - 20
    

5.10 クラス宣言

オブジェクト指向プログラミングのためのクラスをサポートする。 クラスを継承した場合、C ではなく C++ として変換する。

クラスに関して、次の制約を持つ。

  • 多重継承はサポートしない。
  • generics(template) はサポートしない。
  • 全てがオーバーライド可能なメソッドとなる。
    • オーバーライドの抑制はできない。
  • 継承間で引数の異なる同名メソッドは定義できない。
    • ただし、コンストラクタは例外。
pub class Hoge : superClass {
  let pri val : int { pub, pri };
  pub fn __init( arglist ) {
    super( arglist );
  }
  pub fn __free() {
  }
  pub fn func( arglist ) mut : retTypeList {
  }
  pub static fn sub( arglist ) : retTypeList {
  }

  pub override fn proc() : retTypeList {
  }

  let pri data : Other;
  advertise data prefix { whitelist };

  trust ClassB { list };
}

メンバ、メソッドのアクセス制御は pub/pro/pri を使用。 pro は、自分自身と継承しているクラスからアクセスを許可する。

static を付けることで、クラスメソッド、クラスメンバとなる。

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

5.10.1 new

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

let hoge = new Hoge();

5.10.2 メンバ宣言

メンバ宣言は、変数宣言と基本は同じだが以下の点で異なる。

型宣言の後の {} で、アクセッサを宣言できる。

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

let pri val : int { pub, pri };

例えば上記の場合、 メンバ val に対して pub の getter と pri の setter が作られる。 作られる getter と setter は、 get_val(), set_val() のメソッドとなる。 同名のメソッドがある場合は、この宣言は無視される。

5.10.3 メソッド

[pub|pro|pri] [override] fn func( arglist ) mut : retTypeList {
}

メソッドは上記のように宣言する。

アクセス制御とメソッド名、引数と続き、 そのメソッドが mutable な処理を行なうかどうかを宣言し、最後に戻り値の型を宣言する。

メソッド内で自身のメンバ、メソッドにアクセスする場合は self を使用する。

クラスメソッドからクラスメンバにアクセスする場合も、 self を利用する。

override は、メソッドをオーバーライドする際に宣言する。

5.10.4 コンストラクタ

コンストラクタは __init で宣言する。 スーパークラスのコンストラクタをコールする場合は super() を使用する。 super() は、コンストラクタの先頭で呼び出す必要がある。 これは Java と同じ扱い。

コンストラクタ内で、自分自身にアクセスする場合は self を使用する。

5.10.5 デストラクタ

デストラクタは __free で宣言する。 スーパークラスのデストラクタは、サブクラスのデストラクタ実行後に自動でコールされ、 明示的には呼び出せない。

変換後の Lua と C では、デストラクタの実行タイミングが異なる。 Lua では、GC のタイミングで実行する。

5.10.6 advertise

これは、メンバのメソッドを透過的に呼び出せるようにする宣言である。

plantuml_advertise_class.png

例えば上記のようなクラス構造のとき、 次のように Hoge クラスのインスタンスを作成した場合、

Hoge hoge;
hoge.getVal().func();

hoge インスタンス内の val で定義しているメソッドにアクセスするには、 上記のように hoge.getVal().func(); としてアクセスする必要がある。 あるいは val の func() メソッドにアクセスするための wrapper メソッドを、 Hoge クラスに追加する必要がある。

これは非効率と感じる。 特に Hoge クラスにメソッドを追加するのは非常に効率が悪い。

この非効率さが、クラス設計時に本来包含にすべきものを継承としてしまう間違いを 誘発している要因になっていると個人的には感じている。

advertise は、その非効率さを軽減するものである。

advertise することで、そのメンバのメソッドの wrapper メソッドを自動で展開する。

これにより、次のように書ける。

Hoge hoge;
hoge.afunc();

ちなみに afunc() の a は、 advertise 宣言で指定する prefix である。

なお、メンバの全メソッドを公開してしまうのも良くないので、 whitelist として、公開するメソッドのシンボルを列挙できる。 whitelist を指定しない場合は、全ての immutable メソッドを公開する。

advertise で自動で展開した wrapper メソッドのアクセス制御は、 展開元のメソッドと同じとなる。

advertise で公開する wrapper メソッドと同名のメソッドが既にある場合は、 既存のメソッドを優先する。

5.10.7 trust

pub 以外のメンバ、メソッドは外部モジュールからはアクセスできない。

特定クラスに対してこの制限を解除するのが trust である。

trust ClassB { list };

truct に指定したクラスからは、pri, pro の情報にアクセスできる。

公開する pri, pro を制限する場合、list に公開するシンボル名を指定する。

5.10.8 メソッド 呼び出し

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

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

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

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

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

5.10.9 プロトタイプ宣言

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 など同じものを宣言しなければならない。

5.11 マクロ

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

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

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 を使用すること。

5.11.1 macro-statement で利用できる追加 syntax

macro-statement 内では、次の特殊な syntax を追加で利用できる。

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

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

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

,, は、直後に続く を評価する演算子である。 ,,、 ,,,、 ,,,,、 は `{} 内で利用することで、 macro-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 stat3:stat[] = [];
        for index = 1, 4 {
            table.insert( stat3, `{ 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" )                           '' stat2;
for index = 1, 10 do                            
  print( string.format( "hoge %d", index) )     
end
print( string.format( "foo %d", 1) )            '' stat3;
print( string.format( "foo %d", 2) )
print( string.format( "foo %d", 3) )
print( string.format( "foo %d", 4) )
mfunc1( 10 )                                    '' stat4(10)

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

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

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

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

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

5.11.2 マクロの意義

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

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

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

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

これの何が嬉しいのか?

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

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

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

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

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

5.12 モジュール

LuneScript で作成したスクリプトファイルは、全てモジュールとなる。 Lua のように return などは不要。

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

5.12.1 import

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

import はスクリプトの何処でも実行可能で、 import を実行したスコープ内で有効。

import module1;
import module1.ClassA as other;

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

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

また上記の例では、module1.ClassA は other としてリネームされ、 module1.ClassA を other としてアクセス可能となる。

インポートしたシンボルを変数として扱うことは出来ない。

上記の例では、 module1 に対して代入などの演算は出来ない。

5.12.2 require

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

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

require の結果は stem 型となる。

5.12.3 wrap

Lua の外部モジュールの型定義を行なう。

上記の通り、 require で外部モジュールを取り込んだ結果は stem 型になる。 これだと使い勝手が良くない。 これを、解決するのが wrap である。

wrap は Glue のようなもので、 require するモジュールの各メンバ、メソッドの型を宣言することが出来る。

[pub|pro|pri] wrap wrapModule : module {
  pub fn func( arglist ) mut : retTypeList;
}

wrap 内の Syntax は、class と同じ。 ただし、コンストラクタやメソッド等の処理は宣言出来ない。 あくまでも型を宣言するだけである。

なお変換後の Lua では、 wrap によるパフォーマンス低下はない。

6 EBNF

LuneScript の Syntax の EBNF を示す。

chunk ::= block

block ::= {stat} [retstat]

stat ::=  ';' |
	 exp ';' | 
	 'let' ['pub' | 'global'] varlist '=' explist ';'|
	 classdef |
	 'break' ';'|
	 '{' block '}' |
	 'while' exp '{' block '}'|
	 'repeat' '{' block '}' exp ';'|
	 'if' exp '{' block {elseif exp '{' block '}' } [else '{' block] '}' |
	 'for' Name '=' exp ',' exp [',' exp] '{' block '}' |
	 'each' namelist 'in' functioncall '{' block '}' |
	 functiondef

classdef ::= ['pub'] 'class' Name [ ':' Name ] '{' classfieldlist '}'

classfieldlist ::= { memberdef } { methoddef } { advertisedef } { trust }

memberdef ::= ['static'] ['pub' | 'global'] 'let' var [ '{' accessor }' ] ';'

accessor ::= accessorctrl | accessorctrl ',' accessorctrl

accessorctrl ::= 'pub' | 'pri'

methoddef ::= ['static'] accessctrl Name funcbody

accessctrl ::= 'pub' | 'pri' | 'pro'

advertisedef ::= 'advertise' Name Name [ '{' [whitelist] '}' ] ';'

whitelist ::= Name { ',' Name }

trust ::= 'trust' Name [ '{' [whitelist] '}' ] ';'

retstat ::= 'return' [explist] ';'

varlist ::= var {',' var}

var ::=  Name [':' Type]

namelist ::= Name {',' Name}

explist ::= exp {',' exp}

exp ::=  literal | '...' | functiondef | functioncall |

	 prefixexp | tableconstructor | listconstructor |

	 arrayconstructor | exp binop exp | unop exp

literal ::= 'nil' | 'false' | 'true' | Numeral | LiteralString | enumVal

prefixexp ::= var | functioncall | '(' exp ')'

functioncall ::=  prefixexp args | prefixexp ':' Name args

args ::=  '(' [explist] ')' | tableconstructor | listconstructor |
	  arrayconstructor | LiteralString

functiondef ::= ['pub' | 'global'] fn funcbody

funcbody ::= '(' [parlist] ')' [':' typelist ] '{' block '}'

parlist ::= [ arglist ] [',' '...'] | '...'

arglist ::= arg { ',' arg }

arg ::= Name : Type

typelist ::= Type {',' Type}

Type ::= ['&'] ['mut'] typeName ['[]' | '[@]']

typeName ::= userTypeName | builtInTypeName

builtInTypeName ::= 'int' | 'int_' | 'real' | 'real_' | 'enum' |
		    'str' | mapType | 'func' | 'stem'

mapType ::= 'Map' '<' type ',' type '>'

tableconstructor ::= '{' [fieldlist] '}'

arrayconstructor ::= '[@' [explist | literal ':' Numeral ] [ ',' ] ']'

listconstructor ::= '[' [ explist ] [ ',' ] ']'

fieldlist ::= field {',' field} [,]

field ::= '[' exp ']' ':' exp | Name ':' exp | exp

binop ::=  '+' | '-' | '*' | '/' | '//' | '^' | '%' |
	 '&' | '~' | '|' | '>>' | '<<' | '..' |
	 '<' | '<=' | '>' | '>=' | '==' | '~=' |
	 'and' | 'or' | '@'

unop ::= '-' | not | '#' | '~' | '?' | '*'

Author: ifritJP

Created: 2018-09-20 木 21:08

Emacs 24.5.1 (Org mode N/A-fixup)

Validate