公開技術情報

[English] [Japanese]

A. Web ブラウザ上で動作する LuneScript

LuneScript の動作確認用に、Web ブラウザ上で動作する LuneScript 環境を用意しています。

LuneScript 環境には次の2つを用意しています。

  • fengari を利用した環境
  • golang の wasm を利用した環境 (最新)

fengari を利用した環境

https://ifritjp.github.io/LuneScript-webFront/lnsc_web_frontend/for_fengari/

上記リンク先には、次の 3 つの textarea があります。

  • LuneScript のコード入力用
  • 実行結果出力用
  • Lua への変換結果出力用

LuneScript のコード入力用の textarea に LuneScript のコードを入力し、 execute ボタンを押下すると、Lua に変換・実行します。

LuneScript の全ての処理が、ブラウザ上で動作しています。

ただし、次の制限があります。

  • io.open() などのファイル操作が出来ない
  • import() などのモジュールロードが出来ない

LuneScript の全ての処理をブラウザ上で動作させているため、 スマホなどでは初回実行に時間が掛ります。

技術解説

fengari を利用してブラウザ上で Lua VM を動かし、 その Lua VM 上で LuneScript コンパイラを実行しています。

fengari については次の記事を参照してください。

../../lua/fengari/

fengari の Lua VM は、 Lua 上で require すると XMLHttpRequest でそのモジュールをロードします。 LuneScript は 30 個の Lua ファイルで構成しているため、 30 個の Lua ファイルをシーケンシャルにロードすることになります。 30 ファイルのシーケンシャルなロードは流石に効率が悪いため、 事前に XMLHttpRequest で非同期でロードしておき、 require 処理の時は事前にロードしたファイルを load するように処理を切り替えています。

そして LuneScript コンパイラのロード後に、 入力されたユーザの LuneScript コードを LuneScript で変換し、 それを実行します。

LuneScript コンパイラのロードは、そこそこの時間がかかります。 私が使用しているスマホで 10 秒弱、 PC だと 1 秒弱。 一度ロードした後は、ブラウザをリロードするまでは LuneScript のロードは不要で、 ユーザの LuneScript コードの変換、実行が出来ます。

なお、ユーザの LuneScript コードがバグって暴走した時の対処として、 実行後 2 秒経過したら強制停止するようにしています。

この LuneScript コンパイラは、 一度ロードした後は完全にブラウザ内に閉じで動作するため、 サーバ側に負荷は掛りません。 サーバに必要な機能は、静的コンテンツのホスティングだけです。

LuneScript の fengari 対応

fengari Lua VM 上で LuneScript を動かすにあたって、 LuneScript の次の処理を修正しました。

「List<X> 型の foreach 処理を pairs() から ipairs() に切り替え」

オリジナルの Lua VM では、シーケンスのテーブルにおいては、 pairs() でも ipairs() でも列挙する順番が同じであるのに対し、 fengari Lua VM では pairs() では順不同になるようです。 LuneScript では、処理の簡単化と確実性を取って ipairs() ではなく pairs() を利用していましたが、 fengari Lua VM では ipairs() に切り替えました。

なお、この切り替えの制御を –use-ipairs コンパイルオプション で行なうようにしています。 –use-ipairs を指定した場合 ipairs() になります。 現状、オプションを指定していない場合は pairs() を利用していますが、 将来はデフォルト状態を逆にすることも検討しています。

リファレンスのサンプルコード実行

この技術を使用して、 LuneScript リファレンスのサンプルコードを実行するようにしています。

組込み方法は簡単で、次の JavaScript をロードし、

https://ifritjp.github.io/LuneScript-webFront/lnsc_web_frontend/for_fengari/lunescript-front.js

lnsFront.setup(), lnsFront.compile() 関数を実行するだけです。

lnsFront.setup()

lnsFront.setup() は、 fengari と LuneScript をロードし、 各 HTML element の紐付けを行ない、 textarea に格納されている LuneScript コードをコンパイルし、実行します。

setup() は、次の型の関数です。

lnsFront.setup( consoleId, luaCodeId, lnsCodeId, executeId )
引数 意味 必須/Option
consoleId コンソール出力結果を格納する textarea の id 必須
luaCodeId 変換後の Lua コードを格納する textarea の id Option
lnsCodeId Lns コードを格納する textarea の id 必須
executeId 変換を開始するトリガボタンの id Option

上記の Option の element を使用しない場合、 element ID は空文字列を指定してください。

例えば 変換後の Lua コードが不要な場合は、次のように実行します。

var frontId = lnsFront.setup( consoleId, "", lnsCodeId, executeId )

なお、この関数は引数に与えられた consoleId 等をまとめて管理し、 ID を発行して紐付けます。そして、その ID が戻り値となります。

lnsFront.setup() 実行後は、 executeId で指定したボタンをクリックするか、 lnsFront.compile() を実行すると、 登録した lnsCodeId の textarea 内の LuneScript コードを変換して実行し、 実行結果を各 textarea に格納します。

LuneScript のコードを入力する textarea が複数ある場合、 lnsFront.setup() をそれぞれで実行します。

lnsFront.setup() の注意

一つの textarea に対して、lnsFront.setup() を複数回実行しないでください。

一度 lnsFront.setup() を実行した後は、 executeId で登録したボタンをクリックするか、 lnsFront.compile() を実行することで、 登録されている textarea 内の LuneScript のコードを実行します。

lnsFront.compile()

lnsFront.compile() は、 lnsFront.setup() で登録した textarea 内の LuneScript コードを コンパイル・実行します。

lnsFront.compile( frontId, maxTime )
引数 意味 必須/Option
frontId lnsFront.setup() の戻り値 必須
maxTime ユーザの LuneScript 実行のタイムリミット(秒) Option

maxTime を省略した場合、デフォルトの 2 秒がリミットになります。 なお 10 秒以上を指定した場合、無効値として扱い、デフォルト値がセットされます。

golang の wasm を利用した環境

https://ifritjp.github.io/LuneScript-webFront/lnsc_web_frontend/for_wasm/

上記リンク先の構成は、 fengari の構成と比べて次が異なります。

  • LuneScript の実行に fengari ではなく golang の wasm を利用している
  • エディタに textarea ではなく monaco を利用している

技術解説

次に LuneScript の golang wasm を利用するサンプルコードを示します。

<script type="text/javascript" src="./lnsc_frontend.js?symbol=getLnsFrontEnd"></script>
<script>
  addEventListener("load", function( event ) {
    getLnsFrontEnd().then( (frontend) => {
      let result = await frontend.conv2lua( `print( "hello world" );`, {}, true, 4 );
  
      console.log( result.console );
      console.log( result.execLog );
      console.log( result.luaCode );
  } );
} );
</script>

まず、 https://ifritjp.github.io/LuneScript-webFront/lnsc_web_frontend/for_wasm/lnsc_frontend.js を ロードします。

この時に、 ?symbol=getLnsFrontEnd のクエリーを指定しています。

このクエリーは、フロントエンドを取得する関数を登録する際の、 次のシンボル名(getLnsFrontEnd)を指定します。

getLnsFrontEnd().then( (frontend) =>

上記の getLnsFrontEnd の部分をクエリーで指定します。

このスクリプトは、 LuneScript の golang wasm を制御します。

このスクリプトをロードすると、 上記クエリーで指定したシンボル名で関数が追加されます。 クエリーを指定しない場合、 __getLnsFrontEnd のシンボル名で登録されます。 ここでは便宜的に __getLnsFrontEnd のシンボル名を使って説明します。

この関数を実行すると、 LuneScript の golang wasm がロードされます。 なお lnsc_frontend.js は、 LuneScript の golang wasm を worker として動作させます。

__getLnsFrontEnd() は、 async 関数です。 処理が終ると LuneScript の frontend オブジェクトが返されます。

この frontend オブジェクトは、 以下のメソッドを持ちます。

  • async conv2lua( lnsCode, execLua, timeoutSec )
  • async getIndent( lnsCode, targetLineNo, endLineNo )
  • async complete( lnsCode, name2code, lineNo, column )
  • async diag( lnsCode, name2code )
  • async runLnsc( name2code, args )

async conv2lua( lnsCode, name2code, andExec, timeoutSec )

このメソッドは、指定の LuneScript から Lua へトランスコンパイルを行ないます。

引数はそれぞれ

  • lnsCode

    • トランスコンパイル対象の LuneScript コード
  • name2code

    • lnsCode からロードしているモジュール情報。
    • パス名からモジュールのソースコード文字列の Object。
  • execLua

    • トランスコンパイル後の Lua を実行するかどうかの bool。
    • 実行する場合は true。
    • lnsCode が別のモジュールを import ている場合は、現状正常に動作しません。
  • timeoutSec

    • トランスコンパイルから実行までの処理待ちタイムアウト(秒)。
    • 指定時間内に処理が終わらない場合は強制停止させる。
    • lnsCode で指定した LuneScript コードに無限ループの不具合があった場合の対処に利用する。

このメソッドは、次のメンバを持つオブジェクトを返します。

  • luaCode

    • lnsCode をトランスコンパイルした結果の lua コード
  • console

    • トランスコンパイルした際のコンソール出力(warrning や error 情報)
  • execLog

    • lua コードを実行した際の、console 出力結果
    • execLog に false を指定した場合は無効

async getIndent( lnsCode, targetLineNo, endLineNo )

このメソッドは、 指定の LuneScript コードの、指定行のインデント情報を返します。

引数はそれぞれ

  • lnsCode

    • トランスコンパイル対象の LuneScript コード
    • endLineNo の行末に " ___LNS___" の文字列(先頭にスペースがある)を付加してください。
  • targetLineNo

    • インデント量を取得する開始行 (先頭は 1 )
  • endLineNo

    • インデント量を取得する終了行 (先頭は 1 )
    • 一行だけ計算する場合は、 targetLineNo と endLineNo を同じ値にしてください。

このメソッドは、次のようなオブジェクトを返します。

{"indent": {"lines": [
     {"info": {"column": 7,"lineNo": 255}},
     {"info": {"column": 10,"lineNo": 256}},
     {"info": {"column": 10,"lineNo": 257}}]}}

ここで各項目は以下を示します。

  • info

    • targetLineNo と endLineNo の間の行のインデント量を示す
  • lineNo

    • 対象の行番号 (先頭は 1 )
  • column

    • インデント量

async complete( lnsCode, name2code, lineNo, column )

このメソッドは、 指定の LuneScript コードの指定位置の補完情報を返します。

引数はそれぞれ

  • lnsCode

    • トランスコンパイル対象の LuneScript コード。
    • 補完位置に "lune" の文字列を付加してください。
  • name2code

    • lnsCode からロードしているモジュール情報。
    • パス名からモジュールのソースコード文字列の Object。
  • lineNo

    • 補完を行なう位置の行番号 (先頭は 1 )
  • column

    • 補完を行なう位置の column (行頭は 1 )

前提条件として、 対象の lnsCode の lineNo までの処理が正常にビルドできる必要があります。

このメソッドは、次のようなオブジェクトを返します。

{ "complete": {"lunescript": {
    "prefix": "pr",
    "candidateList": [
        {"candidate": {"type": "SymbolKind.Fun","displayTxt": "print(&...)"}}
    ]}}}

ここで各項目は以下を示します。

  • prefix

    • 補完の元になった文字列を示します。
    • 例えば "pr" を補完する情報として "print" が候補として検出された場合、 prefix には "pr" が格納されます
  • candidate

    • 補完候補の情報を示します
  • type

    • シンボルの種別を示します
  • displayTxt

    • 補完する文字列を示します

async diag( lnsCode, name2code )

このメソッドは、 ビルドエラー情報を取得します。

引数はそれぞれ

  • lnsCode

    • トランスコンパイル対象の LuneScript コード。
  • name2code

    • lnsCode からロードしているモジュール情報。
    • パス名からモジュールのソースコード文字列の Object。

このメソッドは、次のようなオブジェクトを返します。

{ "console" : "" }

ここで各項目は以下を示します。

  • console

    • lnsc のビルドエラーメッセージ

async runLnsc( name2code, args )

このメソッドは、 lnsc にコマンドラインオプション args を指定して実行します。

このメソッドは、任意のコマンドラインオプションを指定することが可能です。

引数はそれぞれ

  • name2code

    • lnsCode からロードしているモジュール情報。
    • パス名からモジュールのソースコード文字列の Object。
  • args

    • コマンドラインオプションの文字列配列

このメソッドは、次のようなオブジェクトを返します。

{ "console" : "" }

ここで各項目は以下を示します。

  • console

    • lnsc のコンソール出力