80. Go 言語へのトランスコンパイル
LuneScript はトランスコンパイル先の言語として Lua だけでなく、 Go へのトランスコンパイルが可能です。
この機能により、 LuneScript のセルフホスティングコードを go 言語のコードにトランスコンパイルし、 それを go でビルドすることで、 LuneScript の高速化(約 16倍)を実現しています。
高速化については次を参照してください。 https://ifritjp.github.io/blog2/public/posts/2021/2021-06-28-lunescript-build-time-2000/
ただし、 LuneScript のセルフホスティングに必要な機能に限定したサポートになっているため、 一部の機能は対応できていません。
とはいえ、LuneScript のセルフホスティング自体が、 LuneScript のほとんどの機能を使用しているので、 普通に使う分には問題なく対応出来ています。
go 言語へのトランスコンパイル方法
go 言語へのトランスコンパイルは、 LuneScript のコマンドラインオプション save あるいは SAVE 時に -langGo を指定することで、 .go 拡張子のファイルに go へのトランスコンパイル結果を出力します。
Lua との連携動作
Go 言語へのトランスコンパイル後の構成
LuneScript は Lua へのトランスコンパイラであり、 LuneScript から Lua のコードを直接利用できるという特徴があります。
この特徴は、 Go 言語へトランスコンパイルした後でも引き継がれます。
例えば、次のようなコードを Go 言語へ変換した後でも動かせます。
// @lnsFront: ok
let step = 30;
// Lua のフィボナッチ関数
let code = ```
local function fib ( n )
if n < 2 then return n end
return fib(n - 2) + fib(n - 1)
end
return fib
```;
// Lua コードのロード
form fibFunc( num:int ):int;
fn loadLua( txt:str ) : Luaval<fibFunc> {
let fib;
__luago {
let loaded = unwrap _load( txt ## );
fib = unwrap loaded(##);
}
return fib@@fibFunc;
}
// プロファイル用マクロ
macro _profile( exec:__exp ) {
{}
{
let prev = os.clock();
print( "%s = %d: step = %d: time = %g"
(,,,,exec, ,,exec, step, os.clock() - prev ) );
}
}
// Lua のコードをロードして実行
let fib_lua = loadLua( code );
__luago {
_profile( fib_lua( step ) );
}
// LuneScript のフィボナッチ関数
fn fib_lns( num:int ) : int {
if num < 2 {
return num;
}
return fib_lns( num - 2 ) + fib_lns( num - 1 );
}
// Go で定義した fib_lns 関数を実行
_profile( fib_lns( step ) );
このコードは、 フィボナッチの計算を lua と LuneScript で行なうものです。
最初の code 変数で定義しているのが lua のコードで、
それをロードするのが loadLua()
関数で、ロードされた Lua の関数が fib_lua です。
そして、後半の fib_lns()
関数は フィボナッチの計算を行なう LuneScript の関数です。
Lua へトランスコンパイルした場合
上記コードを Lua にトランスコンパイルして動かすと、次が出力されます。
fib_lua( step ) = 24157817: step = 37: time = 5.69
fib_lns( step ) = 24157817: step = 37: time = 5.38
これは、=fib_lua()= と fib_lns()
それぞれの計算結果と、計算時間を表しています。
これを見ると、
Lua でロードされた関数 fib_lua()
と、
Lns の関数 fib_lns()
の実行時間がほとんど同じ時間掛ることが分かります。
これは当然の結果です。
なぜならば、 LuneScript を Lua に変換した場合は、
LuneScript で書いた fib_lns()
と Lua の fib_lua()
は、
ほとんど同じ Lua のコードになるからです。
上記のコードを Lua にトランスコンパイルした際のコードは次のようになります。
--miniGo.lns
local _moduleObj = {}
local __mod__ = '@miniGo'
local _lune = require( "lune.base.runtime2" )
if not _lune2 then
_lune2 = _lune
end
local step = 37
local code = [==[
local function fib ( n )
if n < 2 then return n end
return fib(n - 2) + fib(n - 1)
end
return fib
]==]
local function loadLua( txt )
local loaded = _lune.unwrap( _lune.loadstring52( txt ))
local fib = _lune.unwrap( loaded( ))
return fib
end
local fib_lua = loadLua( code )
do
local prev = os.clock( )
print( string.format( "%s = %d: step = %d: time = %g", "fib_lua( step )", fib_lua( step ), step, os.clock( ) - prev) )
end
local function fib_lns( num )
if num < 2 then
return num
end
return fib_lns( num - 2 ) + fib_lns( num - 1 )
end
do
local prev = os.clock( )
print( string.format( "%s = %d: step = %d: time = %g", "fib_lns( step )", fib_lns( step ), step, os.clock( ) - prev) )
end
return _moduleObj
注目すべき fib_lns()
関数の定義を抜き出したものが以下です。
local function fib_lns( num )
if num < 2 then
return num
end
return fib_lns( num - 2 ) + fib_lns( num - 1 )
end
これを見ると分かる通り、
fib_lns()
と fib_lub()
は等価のコードになっています。
よって、=fib_lns()= と fib_lua()
が同程度時間がかかるのは当然と言えます。
go へトランスコンパイルした場合
一方、 go へトランスコンパイルした場合、実行結果は次の通りです。
fib_lua( step ) = 24157817: step = 37: time = 6.07
fib_lns( step ) = 24157817: step = 37: time = 0.34
fib_lns()
の方が、=fib_lua()= よりも
計算時間が約 1/18 程度に短かくなっていることが分かります。
go へトランスコンパイルした結果は次の通りです。
// This code is transcompiled by LuneScript.
package lnsc
import . "lnsc/lune/base/runtime_go"
var init_miniGo bool
var miniGo__mod__ string
var miniGo_step LnsInt
var miniGo_code string
var miniGo_fib_lua *Lns_luaValue
type miniGo_fibFunc_1003_ func (arg1 LnsInt) LnsInt
// 14: decl @miniGo.loadLua
func miniGo_loadLua_1009_(txt string) *Lns_luaValue {
var loaded *Lns_luaValue
loaded = Lns_unwrap( Lns_car(Lns_getVM().Load(txt, nil))).(*Lns_luaValue)
var fib LnsAny
fib = Lns_unwrap( Lns_car(Lns_getVM().RunLoadedfunc(loaded,Lns_2DDD([]LnsAny{}))[0]))
return fib.(*Lns_luaValue)
}
// 36: decl @miniGo.fib_lns
func miniGo_fib_lns_1025_(num LnsInt) LnsInt {
if num < 2{
return num
}
return miniGo_fib_lns_1025_(num - 2) + miniGo_fib_lns_1025_(num - 1)
}
func Lns_miniGo_init() {
if init_miniGo { return }
init_miniGo = true
miniGo__mod__ = "@miniGo"
Lns_InitMod()
miniGo_step = 37
miniGo_code = "local function fib ( n )\n if n < 2 then return n end\n return fib(n - 2) + fib(n - 1)\nend\nreturn fib\n"
miniGo_fib_lua = miniGo_loadLua_1009_(miniGo_code)
{
var prev LnsReal
prev = Lns_getVM().OS_clock()
Lns_print([]LnsAny{Lns_getVM().String_format("%s = %d: step = %d: time = %g", []LnsAny{"fib_lua( step )", Lns_getVM().RunLoadedfunc(miniGo_fib_lua,Lns_2DDD(miniGo_step))[0].(LnsInt), miniGo_step, Lns_getVM().OS_clock() - prev})})
}
{
var prev LnsReal
prev = Lns_getVM().OS_clock()
Lns_print([]LnsAny{Lns_getVM().String_format("%s = %d: step = %d: time = %g", []LnsAny{"fib_lns( step )", miniGo_fib_lns_1025_(miniGo_step), miniGo_step, Lns_getVM().OS_clock() - prev})})
}
}
func init() {
init_miniGo = false
}
非常に読み難いので fib_lns()
関数の定義部分を抜き出すと、次になります。
func miniGo_fib_lns_1025_(num LnsInt) LnsInt {
if num < 2{
return num
}
return miniGo_fib_lns_1025_(num - 2) + miniGo_fib_lns_1025_(num - 1)
}
関数名が長くなっていますが、 LuneScript のコードがそのまま go に変換されていることがわかります。 関数を実行する際も次のように普通に関数コールしているだけです。
miniGo_fib_lns_1025_(miniGo_step)
一方で Lua の fib_lua()
関数は、
miniGo_fib_lua = miniGo_loadLua_1009_(miniGo_code)
によって
Lua のコードを実行するための関数がロードされていて、
実行する際も次のように関数コールしています。
Lns_getVM().RunLoadedfunc(miniGo_fib_lua,Lns_2DDD(miniGo_step))
このように、=fib_lns()= と fib_lua()
では全く処理が異なることが分かります。
ビルド
go へトランスコンパイルしてビルドするには次の step が必要です。
- go.mod を生成する
- LuneScript ランタイムを go.mod に登録する
- main.go を生成する
- .lns から .go を生成する
- go.mod を更新する
- go build する
.lns を更新した場合は、 「 .lns から .go を生成する」以降の処理を繰り返し実行します。
各 step を説明します。
go.mod を生成する
lune.js があるディレクトリで、以下のコマンドを実行します。
$ go mod init test # <--- test は環境に合せて指定してください
LuneScript ランタイムを go.mod に登録する
以下のコマンドで、LuneScript のランタイムを go.mod に登録します。
$ go get github.com/ifritJP/LuneScript/src@latest
以降の作業で go mod tidy を実行しますが、必ず go get を先に実行してください。
main.go を生成する
go には、エントリ関数の main()
が必要です。
以下のコマンドで、go の main()
関数を定義する main.go を生成します。
$ lnsc hoge.lns mkmain main.go
ここで hoge.lns は、 lns で __main を定義しているメインモジュールを指定します。 main.go は出力先のパスを指定します。
go にトランスコンパイルする場合、 __main 関数を定義するモジュールが必須です。
このコマンドで生成した main.go には、 lns のランタイムを初期化するコードが含まれます。
.lns から .go を生成する
以下のコマンドで .go を生成します。
$ lnsc hoge.lns save -langGo
__main 関数を含むメインモジュールの場合は、さらに –main オプション付加します。
$ lnsc hoge.lns save -langGo --main hoge
ここで –main hoge の hoge は、 メインモジュールの import パスです。
例えばメインモジュールが foo/bar/hoge.lns の場合、
$ lnsc foo/bar/hoge.lns save -langGo --main foo.bar.hoge
となります。
LuneScript を go にトランスコンパイルする場合、__main 関数が必要です。
go.mod を更新する
以下のコマンドで go.mod を更新します。
$ go mod tidy
go build する
全ての lns ファイルの変換と main.go の生成ができたら、go build を実行します。
$ go build
上記がエラーする場合は、以下を試してください。
$ go build -tags gopherlua
これにより、トランスコンパイルしたモジュールが go でビルドされます。
go へトランスコンパイル後は Lua ライブラリのリンクが必要
追記
Lua ライブラリのリンクを回避する方法を用意しました。
上記の通り、 LuneScript は Lua と密接に連携して動作しています。 そして、go へのトランスコンパイル後もその連携動作をサポートしています。
この連携動作を実現するために、 go へのトランスコンパイル後のコードでは、 lua の処理を行なうための lua VM を必要とします。
細かいことを言うと、この Lua との連携動作以外にも、 LuneScript には Lua VM を必要とする処理があります。 具体的にはマクロ展開の処理や、一部の組込み関数の処理に Lua VM を利用しています。
その lua VM は、公式の lua-5.3.4 を前提としていて、 liblua5.3.so をリンクします。
go 言語には、 「環境に依存しない 1 つの実行ファイルを生成できる」という利点がありますが、 残念ながら LuneScript を go へトランスコンパイルする場合、 liblua5.3.so へのリンクが必要になります。
なお、実行時に liblua5.3.so が必要になるだけではなく、 ビルド時には lua-5.3.4 のインクルードファイルも必要になります。