公開技術情報

[English] [Japanese]

Z. Go 言語へのトランスコンパイル (検討段階)

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 の扱いが楽になるが、次の制限がある。

上記から、 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))