tech

[English] [Japanese]

Z. Transcompile to Go language (exploratory stage)

For information on transcompiling to Go language, please refer to the following article.

../go

The information here is old, but I'll keep it for reference.

Considering transcompiling from LuneScript to Go language.

This is where you record what you are discussing.

aim

The goals of transcompiling from LuneScript to Go language are as follows.

  • Speeding up LuneScript

    • LuneScript supports the code completion function, but it becomes heavy and inconvenient for large-scale code.
    • Improve the usability of LuneScript's code completion feature by speeding it up.
  • Improved efficiency of LuneScript version upgrade work by speeding up

    • LuneScript's self-hosted build/test time currently takes less than 2 minutes.
    • It takes 2 minutes for each change, so I try to speed it up with Go.
    • We were working on transcompiling to C language, but the implementation of GC in C language did not proceed as expected, so Go, which supports GC at the language level, will be supported first.

value

Types in LuneScript Go types type in Lns Random for Go
nil, null interface{} nil
int int LnsInt (alias)
real float64 LnsReal(alias)
str string LnsStr (alias)
bool bool LnsBool(alias)
List unique structure LnsList
Array arrangement LnsArray
Map map LnsMap
Set map (put item in key) LnsSet
class Structure
interface interface
form func LnsForm
enum int/float64/string LnsEnum
stem interface{} LnsStem
nilable interface{}

Handling int/real

LuneScript int/real is defined using go's type alias as follows:

type LnsInt = int
type LnsReal = float64

Handling nilables

LuneScript's nilable cannot be treated as the original value as is. However, you can still check for equality. This is the same as go and LuneScript.

Dealing with boolean values

LuneScript is false for nil and false and true otherwise. Since go requires judgment by true/false, prepare a condition conversion function for LuneScript.

Handling and/or

LuneScript's and/or is not a logical operation, it controls the evaluation of expressions, and the evaluation result is not bool. go evaluates to bool.

Handling of generics

Since go does not have generics, all types of generics are handled with interface{}.

Dealing with Sets

Since go doesn't have Set, we use Map instead.

return multiple values

Multi-value returns in go and LuneScript behave differently.

When hoge() is a multi-value return function that returns x and y, the behavior is as follows.

code LuneScript expansion result go expansion result
(hoge()) x x, y
hoge(), val x, val x, y, val

To realize the above difference, go prepares the following conversion functions.

// 多値返却の先頭 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)
}

classes and inheritance

Go has structs and receivers, but no inheritance.

LuneScript has inheritance, so we need to implement inheritance in Go.

Shows how to support the following LuneScript classes in 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;
   }
}

Equivalent code in Go

Show equivalent code in 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 ) )
}

Inheritance implementation method

Parent class

First, we will explain the Parent class.

// @lnsFront: ok
class Parent {
   let val1:int;
   pub fn sub1():int {
      return self.val1;
   }
}
data structure

Define the following structure and interface to represent the Parent class.

type ParentMtd interface {
    sub1 () int
}
type Parent struct {
    val1 int
    FP ParentMtd
}
type ParentDownCast interface {
    ToParent() *Parent
}
func (obj *Parent ) ToParent() *Parent {
    return obj
}
  • The ParentMtd interface is responsible for

    • Define a method in the Parent class
    • Express the morphism of the Parent class
  • A Parent struct has a member and a ParentMtd
  • ParentDownCast is defined per class for downcasting
method

Define the following receiver function to represent the method of the Parent class.

func (self *Parent) sub1() int {
    return self.val1
}
constructor

Define the following as a constructor of Parent class.

func NewParent(val1 int) *Parent {
    super := &Parent{ val1, nil }
    super.FP = super
    return super
}

This constructor does the following:

  • member initialization
  • FP settings
How to use the Parent class

Parent is used like this:

parent := NewParent( 1 )
print( parent.FP.sub1() )

When calling a method, always call it through the FP interface.

Sub class

Describe the Sub class.

// @lnsFront: skip
class Sub extend Parent {
   let val2:int;
   pub override fn sub1():int {
      return self.val2;
   }
   pub fn sub2():int {
      return self.val2;
   }
}
data structure

Define the following structure and interface to represent the Sub class.

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
}
  • The SubMtd interface declares the methods defined in Sub.

    • Do not include Parent methods
  • The Sub structure declares the data of the Parent structure and the members defined in Sub.
method

Define the following receiver function to represent the method of the Sub class.

func (self *Sub) sub1() int {
    return self.val2
}
func (self *Sub) sub2() int {
    return self.val2
}
constructor

Define the following as a constructor of Sub class.

func newSub(val1,val2 int) *Sub {
    sub := &Sub{ Parent{ val1, nil }, val2, nil }
    sub.Parent.FP = sub
    sub.FP = sub
    return sub
}

This constructor does the following:

  • member initialization
  • FP settings

    • FP of super is also set here
    • Set the FP of this super to &sub instead of &super to achieve polymorphism

SubSub class

Describe the SubSub class.

// @lnsFront: skip
class SubSub extend Sub {
   let val3:int;
   pub override fn sub1():int {
      return self.val3;
   }
   pub fn sub3():int {
      return self.val3;
   }
}
data structure

Define the following structure and interface to represent the SubSub class.

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
}
  • The SubSubMtd interface declares the methods defined by SubSub.

    • Do not include Sub methods
  • The SubSub structure declares the data of the Sub structure and the members defined by SubSub.
method

Define the following receiver function to represent the methods of the SubSub class.

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
}
Method definition without overriding

It should be noted that the sub2() method calls self.Sub.sub2().

The SubSub class does not override the sub2 method. In other words, the sub2 method of SubSub uses the method of the Sub class. So we are calling the Sub.sub2 method.

constructor

Define the following as a constructor of SubSub class.

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
}

This constructor does the following:

  • member initialization
  • FP settings

    • Also set the FP of Parent and Sub here
    • Implement polymorphism by setting &subsub to FP of Parent and Sub

IF interface

// @lnsFront: ok
interface IF {
  pub fn sub1():int;
}
data structure

LuneScript's interface uses Go's interface as it is.

interface IF {
  pub fn sub1():int;
}

method call

To call a method of the Parent class, do the following:

func test(parent *Parent) int {
  print( parent.FP.sub1() )
  print( parent.sub1() )
}
Difference between parent.FP.sub1() and parent.sub1()

There are two patterns for method calls:

  • parent.FP.sub1()

    • Method calls that support polymorphism
  • parent.sub1()

    • Method call defined in Parent class

      • No support for polymorphism
overhead
  • Method calls that support polymorphism have a large overhead.
  • Method calls that support polymorphism should be limited to cases where polymorphism is necessary.
  • Whether or not polymorphism is required is currently not defined in LuneScript.

    • It is necessary to introduce final declarations for classes and methods so that polymorphism can be clearly stated.

up-cast / down-cast

  • up-cast is achieved by accessing the embedded pointer

    • up-casting to an interface uses the interface type held by the object
  • A down-cast implements an interface with a type assertion.

    • Define a DownCast interface for each class, cast to that interface, and then execute the cast function to the target class
var ifObj IF = obj.FP // インタフェースをセットする
parent := &obj.Parent // アップキャスト
(parent.FP.(SubDownCast)).ToSub() // obj を Sub にダウンキャストする

Summary of Classes

  • Declare an interface that defines the methods of the class

    • Embed the interface of the method defined in the Super class
type TestMtd interface {
    SuperMtd
    method() int
}
  • Declare a struct to hold the members of the class and the above interface

    • Inheritance embeds the inheriting type
type Test struct {
    Super
    val int
    FP TestMtd
}
  • Define interface for downcast
type TestDownCast interface {
    ToTest() *Test
}
  • Define a method for downcasting

    • This method declares everything for the Super class
func (obj *SubSub) ToSub() *Sub {
    return &obj.Sub
}
  • Declare receivers that define the behavior of the methods of the class

    • Declare the receiver including the methods of the Super class
    • A non-overridden function calls the method of the struct that defines it
func (self *Test) method() int {
    return self.super.method()
}
  • Initialize members and interface FP in the constructor

    • interface initializes including the interface FP of the Super class
  • Method calls are called via interface FP

    • Method calls without polymorphism call the struct's methods directly
obj.FP.method() // ポリモーフィズム有効
obj.method()    // ポリモーフィズム無効
  • up-casting is accomplished by accessing the member's Super class pointer

    • up-casting to an interface uses the interface type held by the object
  • down-cast implements an interface with a type assertion and an interface for downcasting.
var ifObj IF = obj.FP // インタフェースをセットする
super := &obj.super // アップキャスト
(parent.FP.(SubDownCast)).ToSub() // obj を Sub にダウンキャストする
  • The interface uses Go's interface as it is

    • Use interface FP when up-casting from a class object to an interface

symbol name

Symbol names in LuneScript and go differ significantly in the following ways.

  • namespace

    • LuneScript is in the same file (module)
    • go is in the same directory (package)
  • Public/private control method

    • LuneScript is controlled by pub/pro etc.
    • go controls the case of the first letter of a symbol

This difference causes the following problems:

  • When defining a symbol sym with the same name in different files FileA.lns and FileB.lns in LuneScript, when converting this to go, if you define a symbol sym with the same name in FileA.go and FileB.go with the same configuration , the symbol sym will result in a duplicate definition error.
  • Symbols defined publicly in lowercase in LuneScript become private in go.

    • Symbols defined privately in uppercase in LuneScript are public in go.

To work around this problem, handle symbol names as follows:

Add the file name to the beginning of the symbol of the function or class that is subject to public/private control. Add G (G for GLOBAL) if it is public, and l (l for local) if it is private.

In other words, if you convert the following LuneScript source to go,

// @lnsFront: ok
fn func() {
}
pub class Class {
   let val1:int;
   pub let val2:int;
}

The relationship between LuneScript and go symbols is as follows.

public/private lns go
private func lfile_func
Release Class Gfile_Class
private val1 lval1
Release val2 Gval2

Arguments and local variables are basically converted as they are because there is no difference in scope between LuneScript and go.

Lua VM

In the current LuneScript, Lua VM is used during Macro expansion. There are two ways to use the Lua VM with Go:

  • Use gopher-lua, a port of Lua to Go
  • use liblua

Using gopher-lua makes dealing with the Lua VM easier, but it has the following limitations:

From the above, the LuneScript transcompiler uses liblua.

cgo

Use cgo to use liblua from Go.

cgo is a package for calling C language libraries from Go.

The C code written in the comment before import "C" is parsed and expanded into a C package so that it can be accessed by Go, like so:

// #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' )" )
}

Since cgo does not support #define macro functions, functions with macro definitions such as the following must be expanded and processed by yourself.

#define luaL_dostring(L, s) \
	(luaL_loadstring(L, s) || lua_pcall(L, 0, LUA_MULTRET, 0))