Z. Go 言語へのトランスコンパイル (検討段階)
Go 言語へのトランスコンパイル対応については、次の記事を参考に。
ここの情報は古いが、参考程度に残しておく。
LuneScript から Go 言語へのトランスコンパイルを検討中。
ここでは、検討中の内容を記録する。
ねらい
LuneScript から Go 言語へのトランスコンパイルの狙いは次の通り。
-
LuneScript の動作高速化
- LuneScript はコード補完機能をサポートしているが、 規模の大きいコードだと重くなって使い勝手が悪い。
- 高速化することで、LuneScript のコード補完機能の使い勝手を向上させる。
-
高速化による、 LuneScript 自体のバージョンアップ作業効率改善
- LuneScript のセルフホストのビルド・テスト時間が、現状 2 分弱掛っている。
- 変更する毎に 2 分かかるのは辛いので、Go で高速化を図る。
- C 言語へのトランスコンパイルの対応を進めていたが、 C 言語での GC 実装が思うように進まなかったので、 言語レベルで GC 対応している Go を先に対応する。
値
LuneScriptの型 | Go の型 | Go 用 Lns ランタム での type |
---|---|---|
nil, null | interface{} | nil |
int | int | LnsInt (alias) |
real | float64 | LnsReal(alias) |
str | string | LnsStr (alias) |
bool | bool | LnsBool(alias) |
List | 独自構造体 | LnsList |
Array | 配列 | LnsArray |
Map | map | LnsMap |
Set | map (key に item を入れる) | LnsSet |
class | 構造体 | |
interface | interface | |
form | func | LnsForm |
enum | int/float64/string | LnsEnum |
stem | interface{} | LnsStem |
nilable | interface{} |
int/real の扱い
LuneScript int/real は、 go の type alias を使用して次のように定義する。
type LnsInt = int
type LnsReal = float64
nilable の扱い
LuneScript の nilable は、そのままでは元の値として扱えない。 しかし、等しいかどうかのチェックは行なえる。 これは go と LuneScript と同じ。
真偽値の扱い
LuneScript は nil と false が偽で、それ以外は真となる。 go は true/false で判断が必要なので、 LuneScript 用の条件変換関数を用意する。
and/or の扱い
LuneScript の and/or は論理演算ではなく、式の評価を制御し、評価結果も bool ではない。 go は評価結果が bool になる。
Generics の扱い
go は Generics がないので、 Generics の型は全て interface{} で扱う。
Set の扱い
go は Set がないので、 Map で代用する。
多値返却
go と LuneScript の多値返却は動きが異なる。
hoge()
が x, y を返す多値返却の関数としたとき、それぞれ次の動きになる。
コード | LuneScript 展開結果 | go 展開結果 |
---|---|---|
(hoge()) | x | x, y |
hoge(), val | x, val | x, y, val |
上記違いを実現するために、 go では次の変換関数を用意する。
// 多値返却の先頭 int を返す
func carInt( multi ...interface{} ) int {
if len( multi ) == 0 {
panic( "nothing" )
}
return multi[0].(int)
}
// 多値返却の先頭 int! を返す
func carIntN( multi ...interface{} ) interface{} {
if len( multi ) == 0 {
return nil
}
if multi[0] == nil {
return nil
}
return multi[0].(int)
}
クラスと継承
Go は構造体とレシーバはあるが、継承がない。
LuneScript は継承を持つので、 Go で継承を実現する必要がある。
次の LuneScript のクラスを Go で対応する方法を示す。
// @lnsFront: ok
interface IF {
pub fn sub1():int;
}
class Parent {
let val1:int;
pub fn sub1():int {
return self.val1;
}
}
class Sub extend Parent {
let val2:int;
pub override fn sub1():int {
return self.val2;
}
pub fn sub2():int {
return self.val2;
}
}
class SubSub extend Sub {
let val3:int;
pub override fn sub1():int {
return self.val3;
}
pub fn sub3():int {
return self.val3;
}
}
Go による等価コード
Go による等価コードを示す
package main
import "fmt"
type ParentMtd interface {
sub1 () int
}
type Parent struct {
val1 int
FP ParentMtd
}
type ParentDownCast interface {
ToParent() *Parent
}
func (obj *Parent ) ToParent() *Parent {
return obj
}
func (self *Parent) sub1() int {
return self.val1
}
func NewParent(val1 int) *Parent {
parent := Parent{ val1, nil }
parent.FP = &parent
return &parent
}
type SubMtd interface {
ParentMtd
sub2 () int
}
type Sub struct {
Parent
val2 int
FP SubMtd
}
type SubDownCast interface {
ToSub() *Sub
}
func (obj *Sub ) ToSub() *Sub {
return obj
}
func (self *Sub) sub1() int {
return self.val2
}
func (self *Sub) sub2() int {
return self.val2
}
func newSub(val1,val2 int) *Sub {
sub := Sub{ Parent{ val1, nil }, val2, nil }
sub.Parent.FP = &sub
sub.FP = &sub
return &sub
}
type SubSubMtd interface {
SubMtd
sub3 () int
}
type SubSub struct {
Sub
val3 int
FP SubSubMtd
}
type SubSubDownCast interface {
ToSubSub() *SubSub
}
func (obj *SubSub ) ToSubSub() *SubSub {
return obj
}
func (obj *SubSub ) ToSub() *Sub {
return &obj.Sub
}
func (self *SubSub) sub1() int {
return self.val3
}
func (self *SubSub) sub2() int {
return self.Sub.sub2()
}
func (self *SubSub) sub3() int {
return self.val3
}
func newSubSub(val1,val2,val3 int) *SubSub {
subsub := SubSub{ Sub{ Parent{ val1, nil }, val2, nil }, val3, nil }
subsub.Parent.FP = &subsub
subsub.Sub.FP = &subsub
subsub.FP = &subsub
return &subsub
}
func testParent( obj *Parent ) {
fmt.Println( obj.FP.sub1() )
}
func testSub( mess string, obj *Sub ) {
fmt.Println( mess, obj.FP.sub1(), obj.FP.sub2() )
}
func testCast( obj *Parent ) {
cast, ok := obj.FP.(SubDownCast)
if ok {
testSub( "cast", cast.ToSub() )
} else {
fmt.Println( "cast NG" )
}
}
func Lns_init() {
subsub := newSubSub( 1, 2, 3 )
fmt.Println( subsub.val1, subsub.val2, subsub.val3 )
fmt.Println( subsub.FP.sub1(), subsub.FP.sub2(), subsub.FP.sub3() )
testSub( "subsub.Sub", &subsub.Sub )
testParent( &subsub.Parent )
testCast( &subsub.Parent )
sub := newSub( 1, 2 )
testSub( "sub", sub )
testParent( &sub.Parent )
testCast( &sub.Parent )
testCast( NewParent( 1 ) )
}
継承実現方法
Parent クラス
まず、 Parent クラスについて説明する。
// @lnsFront: ok
class Parent {
let val1:int;
pub fn sub1():int {
return self.val1;
}
}
データ構造
Parent クラスを表現するため、次の構造体と interface を定義する。
type ParentMtd interface {
sub1 () int
}
type Parent struct {
val1 int
FP ParentMtd
}
type ParentDownCast interface {
ToParent() *Parent
}
func (obj *Parent ) ToParent() *Parent {
return obj
}
-
ParentMtd インタフェースは、次の役割を持つ
- Parent クラスのメソッドを定義
- Parent クラスのモリモーフィズムを表現する
- Parent 構造体は、メンバと ParentMtd を持つ
- ParentDownCast は、ダウンキャスト用にクラスごとに定義する
メソッド
Parent クラスのメソッドを表現するため、次のレシーバ関数を定義する。
func (self *Parent) sub1() int {
return self.val1
}
コンストラクタ
Parent クラスのコンストラクタとして、次を定義する。
func NewParent(val1 int) *Parent {
super := &Parent{ val1, nil }
super.FP = super
return super
}
このコンストラクタは次の処理を行なう。
- メンバの初期化
- FP の設定
Parent クラスの使用方法
Parent は次のように使用する。
parent := NewParent( 1 )
print( parent.FP.sub1() )
メソッドをコールする場合、必ず FP インタフェースを介してコールする。
Sub クラス
Sub クラスについて説明する。
// @lnsFront: skip
class Sub extend Parent {
let val2:int;
pub override fn sub1():int {
return self.val2;
}
pub fn sub2():int {
return self.val2;
}
}
データ構造
Sub クラスを表現するため、次の構造体と interface を定義する。
type SubMtd interface {
ParentMtd
sub2 () int
}
type Sub struct {
Parent
val2 int
FP SubMtd
}
type SubDownCast interface {
ToSub() *Sub
}
func (obj *Sub ) ToSub() *Sub {
return obj
}
func (obj *Sub ) ToParent() *Parent {
return &obj.Parent
}
-
SubMtd インタフェースは、 Sub で定義しているメソッドを宣言する。
- Parent のメソッドは含めない
- Sub 構造体は、Parent 構造体のデータと、 Sub で定義しているメンバを宣言する。
メソッド
Sub クラスのメソッドを表現するため、次のレシーバ関数を定義する。
func (self *Sub) sub1() int {
return self.val2
}
func (self *Sub) sub2() int {
return self.val2
}
コンストラクタ
Sub クラスのコンストラクタとして、次を定義する。
func newSub(val1,val2 int) *Sub {
sub := &Sub{ Parent{ val1, nil }, val2, nil }
sub.Parent.FP = sub
sub.FP = sub
return sub
}
このコンストラクタは次の処理を行なう。
- メンバの初期化
-
FP の設定
- super の FP もここで設定する
- この super の FP に &super ではなく &sub を設定することで、ポリモーフィズムを実現する
SubSub クラス
SubSub クラスについて説明する。
// @lnsFront: skip
class SubSub extend Sub {
let val3:int;
pub override fn sub1():int {
return self.val3;
}
pub fn sub3():int {
return self.val3;
}
}
データ構造
SubSub クラスを表現するため、次の構造体と interface を定義する。
type SubSubMtd interface {
SubMtd
sub3 () int
}
type SubSub struct {
Sub
val3 int
FP SubSubMtd
}
type SubSubDownCast interface {
ToSubSub() *SubSub
}
func (obj *SubSub ) ToSubSub() *SubSub {
return obj
}
func (obj *SubSub ) ToSub() *Sub {
return &obj.Sub
}
func (obj *SubSub ) ToParent() *Parent {
return &obj.Parent
}
-
SubSubMtd インタフェースは、 SubSub で定義しているメソッドを宣言する。
- Sub のメソッドは含めない
- SubSub 構造体は、Sub 構造体のデータと、 SubSub で定義しているメンバを宣言する。
メソッド
SubSub クラスのメソッドを表現するため、次のレシーバ関数を定義する。
func (self *SubSub) sub1() int {
return self.val3
}
func (self *SubSub) sub2() int {
return self.Sub.sub2()
}
func (self *SubSub) sub3() int {
return self.val3
}
override していないメソッド定義
上記で注目すべきは、 sub2() メソッドで self.Sub.sub2()
をコールしている点。
SubSub クラスは、 sub2 メソッドを override していない。 つまりは、SubSub の sub2 メソッドは Sub クラスのメソッドが使用されることになる。 よって、 Sub.sub2 メソッドをコールしている。
コンストラクタ
SubSub クラスのコンストラクタとして、次を定義する。
func newSubSub(val1,val2,val3 int) *SubSub {
subsub := &SubSub{ Sub{ Parent{ val1, nil }, val2, nil }, val3, nil }
subsub.Parent.FP = subsub
subsub.Sub.FP = subsub
subsub.FP = subsub
return subsub
}
このコンストラクタは次の処理を行なう。
- メンバの初期化
-
FP の設定
- Parent, Sub の FP もここで設定する
- Parent, Sub の FP に &subsub を設定することで、ポリモーフィズムを実現する
IF インタフェース
// @lnsFront: ok
interface IF {
pub fn sub1():int;
}
データ構造
LuneScript の interface は、 Go のインタフェースをそのまま使用する。
interface IF {
pub fn sub1():int;
}
メソッド呼び出し
Parent クラスのメソッドを呼び出すには、次のように行なう。
func test(parent *Parent) int {
print( parent.FP.sub1() )
print( parent.sub1() )
}
parent.FP.sub1() と parent.sub1() の違い
メソッド呼び出しには、次の 2 つのパターンがある。
-
parent.FP.sub1()
- ポリモーフィズムに対応したメソッド呼び出し
-
parent.sub1()
-
Parent クラスに定義しているメソッド呼び出し
- ポリモーフィズムに非対応
-
オーバーヘッド
- ポリモーフィズムに対応したメソッド呼び出しは、オーバーヘッドが大きい。
- ポリモーフィズムに対応したメソッド呼び出しは、 ポリモーフィズムが必要な場合に限定すべき。
-
ポリモーフィズムが必要かどうかは、 LuneScript では現状定義がない。
- クラスとメソッドに final 宣言を導入 し、 ポリモーフィズムが不要であることを明示できるようにする対応が必要
up-cast / down-cast
-
up-cast は、埋め込みのポインタにアクセスすることで実現する
- インタフェースへの up-cast は、オブジェクトが保持する interface 型を使用する
-
down-cast は、 interface を型アサーションで実現する。
- 各クラスごとに DownCast インタフェースを定義して、 そのインタフェースにキャストしてから、 さらに目的のクラスへのキャスト関数を実行する
var ifObj IF = obj.FP // インタフェースをセットする
parent := &obj.Parent // アップキャスト
(parent.FP.(SubDownCast)).ToSub() // obj を Sub にダウンキャストする
Class のまとめ
-
クラスのメソッドを定義する interface を宣言する
- Super クラスで定義しているメソッドの interface を埋め込む
type TestMtd interface {
SuperMtd
method() int
}
-
クラスのメンバと前記の interface を保持する構造体を宣言する
- 継承は、 継承する型を埋め込む
type Test struct {
Super
val int
FP TestMtd
}
- ダウンキャスト用の interface を定義する
type TestDownCast interface {
ToTest() *Test
}
-
ダウンキャスト用のメソッドを定義する
- このメソッドは、 Super クラスの分を全て宣言する
func (obj *SubSub) ToSub() *Sub {
return &obj.Sub
}
-
クラスのメソッドの動作を定義するレシーバを宣言する
- レシーバは Super クラスのメソッドを含めて宣言する
- override していない関数は、そのメソッドを定義している構造体のメソッドをコールする
func (self *Test) method() int {
return self.super.method()
}
-
コンストラクタで、メンバの初期化と interface FP を初期化する
- interface は、 Super クラスの interface FP を含めて初期化する
-
メソッド呼び出しは、 interface FP 経由でコールする
- ポリモーフィズム無効なメソッド呼び出しは、構造体のメソッドを直接コールする
obj.FP.method() // ポリモーフィズム有効
obj.method() // ポリモーフィズム無効
-
up-cast は、メンバの Super クラスのポインタにアクセスすることで実現する
- インタフェースへの up-cast は、オブジェクトが保持する interface 型を使用する
- down-cast は、 interface を型アサーションと、ダウンキャスト用 interface で実現する。
var ifObj IF = obj.FP // インタフェースをセットする
super := &obj.super // アップキャスト
(parent.FP.(SubDownCast)).ToSub() // obj を Sub にダウンキャストする
-
インタフェースは、 Go の interface をそのまま利用する
- クラスオブジェクトからインタフェースに up-cast する場合、 interface FP を使用する
シンボル名
LuneScript と go のシンボル名は、次の点で大きく異なる。
-
名前空間
- LuneScript は、同一ファイル(モジュール)内
- go は、同一ディレクトリ(パッケージ)内
-
公開・非公開の制御方法
- LuneScript は、 pub/pro などで制御する
- go は、 シンボルの先頭文字の大文字/小文字で制御する
この違いによって次の問題が発生する。
- LuneScript で異なるファイル FileA.lns, FileB.lns それぞれに 同名のシンボル sym を定義している時、 これを go に変換する際に同じ構成で FileA.go, FileB.go に同名のシンボル sym を定義すると、 シンボル sym が重複定義エラーとなる。
-
LuneScript で小文字で公開定義したシンボルが、 go では非公開になる。
- LuneScript で大文字で非公開定義したシンボルが、 go では公開になる。
この問題に対応するため、次の通りシンボル名を処理する。
公開・非公開制御対象の関数あるいはクラスのシンボルの先頭にファイル名を付加する。 さらに公開なら G ( GLOBAL の G)、非公開なら l (local の l) を付加する。
つまり次の LuneScript のソースを go に変換する場合、
// @lnsFront: ok
fn func() {
}
pub class Class {
let val1:int;
pub let val2:int;
}
LuneScript と go のシンボルの関係は次のようになる。
公開/非公開 | lns | go |
---|---|---|
非公開 | func | lfile_func |
公開 | Class | Gfile_Class |
非公開 | val1 | lval1 |
公開 | val2 | Gval2 |
なお、引数やローカル変数は LuneScript と go とでスコープに違いはないため、 基本的にそのまま変換する。
Lua VM
現状の LuneScript では、 Macro 展開時に Lua VM を使用する。 Go で Lua VM を使用する方法として、次の 2 つがある。
- Lua を Go に移植した gopher-lua を使用する
- liblua を使用する
gopher-lua を使用すると Lua VM の扱いが楽になるが、次の制限がある。
- Lua VM のバージョンが Lua5.1 になる
-
liblua と比べると遅い
-
公式 Wiki(<https://github.com/yuin/gopher-lua/wiki/Benchmarks>) の情報によると、 fib(35) の実行時間が次になる。
- lua5.1.4
- 1.71sec
- Gopherlua
- 5.40sec
-
上記から、 LuneScript のトランスコンパイラでは liblua を利用する。
cgo
Go から liblua を利用するため cgo を使う。
cgo は Go から C 言語のライブラリをコールするためのパッケージ。
以下のように import "C" の前のコメントに書いた C コードが解析され、 Go からアクセスできるように C パッケージ内に展開される。
// #include <stdlib.h>
// #cgo CFLAGS: -I/usr/include/lua
// #cgo LDFLAGS: -ldl -lm -llua
// #include <lauxlib.h>
// #include <lualib.h>
import "C"
import "unsafe"
// lua のコードを実行する
func lua_runScript( script string ) {
var vm * C.lua_State = C.luaL_newstate()
if vm == nil {
return
}
defer C.lua_close( vm )
C.luaL_openlibs( vm )
block := C.CString( script )
defer C.free( unsafe.Pointer( block ) )
C.luaL_loadstring( vm, block )
C.lua_pcallk( vm, 0, C.LUA_MULTRET, 0, 0, nil )
}
func main() {
lua_runScript( "print( 'hello world' )" )
}
cgo は #define マクロ関数には対応していないので、 次のようなマクロ定義された関数は、自前で展開して処理を書かなければならない。
#define luaL_dostring(L, s) \
(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))