Kinx ライブラリ - REPL


Kinx ライブラリ - REPL

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は REPL です。

当初、REPL できないんじゃないか、と悲観していましたが、なんとなくそれっぽく動くものが出来上がったのでご紹介です。

というのも、evel を素直に実行すると、1回実行するたびにコード領域が半端なく膨れ上がっていくので、以前紹介した Isolate と同じように別コンテキストで実行させるようにしました。そうすることで劇的にメモリ使用量が改善されました。

ところが、これだとコンテキストが別なので オブジェクトの受け渡しができない のです。これもクラス定義や関数定義を毎回 eval に引き渡すことでなんとかやりくりするようにしました。その結果、、、

  1. 一応計算機程度には使えるものになった。
  2. 関数(function/native)、クラス、モジュールもそれなりに使える。
  3. エスケープシーケンスで Windows でも カラフルに。
  4. [TAB] キーでのキーワード補完も頑張ってみた。
  5. East Asian Width のサポート。UTF8 文字も [BS] とか [DEL] とかいけるはず。少なくとも日本語はOK。

という程度には完成しました。頑張った...

What is REPL?

「REPL ってなあに?」、=> Read-Eval-Print-Loop です。入力を読み取って eval()(評価)して、その結果を表示する、というのを繰り返します。

使い方

起動方法

REPL は SpecTest と同じように Kinx 本体に組み込まれています。以下のように実行します。

$ ./kinx --exec:repl
kinx[  0]> _

ここでは Markdown で記載しているので色が異なりますが、実際はプロンプトやキーワードなどがハイライトされ、カラー表示されます(Windows も Linux も)。

それを分かってもらうために最初は画像を張ってみたのですが、やはりデモだ、ということで、デモを頑張って作ってみました。誰か褒めてください

オートコンプリート(キーワード補完)もちゃんと動いていることがわかります。この辺はデモとかが無いと理解が難しいですよね。百聞は一見に如かず。

サンプル

手始めに、ここ(20分ではじめるRuby) に載っているのを試してみましょう。よかった、この程度ならちゃんとできてる。

$ ./kinx --exec:repl
kinx[  0]> "Hello, world"
=> "Hello, world"

kinx[  1]> System.println("Hello, world")
Hello, world
=> (null)

kinx[  2]> 3+2
=> 5

kinx[  3]> 3*2
=> 6

kinx[  4]> 3**2
=> 9

kinx[  5]> Math.sqrt(9)
=> 3

kinx[  6]> a = 3 ** 2
=> 9

kinx[  7]> b = 4 ** 2
=> 16

kinx[  8]> Math.sqrt(a+b)
=> 5

では本題です。

コマンド一覧

コマンドは全て . で始まります。

コマンド 内容
.help ヘルプの表示
.quit 終了
.history コマンド履歴の表示
.vars 現在の変数一覧表示
.delete name 現在の変数一覧から name の変数を削除
.showdef name 関数やクラスの定義を表示
.cursor [*on/off] カーソル表示の ON/OFF(デフォルト ON)
.fullcode [on/*off] フル入力モード(デフォルト OFF)、.run コマンドが入力されるまでは実行しない
.time [on/*off] 実行時間計測モード(デフォルト OFF)、実行後に実行時間を表示
.run フル入力モード時に実行させるために使用

尚、Ctrl-C でも終了してしまいますので、その点はご了承ください。

.help と打つと、以下のヘルプが表示されます。

kinx[  0]> .help
Kinx REPL Command: * means by default
    .help               Display this help.
    .quit               Quit REPL.
    .history            Display command history.
    .vars               Display variables with its value.
    .delete name        Delete a variable by name.
    .showdef name       Display function/class definition by name.
    .cursor [*on|off]   Set to 'off' to make the cursor invisible.
    .fullcode [on|*off] Set to 'on', and the code will be executed by .run instead of immediately.
    .time [on|*off]     Set to 'on' to measure and display elapsed time.
    .run                Execute the code only with .fullcode 1.

REPL Operation:
    [^] Arrow up        Choose a previous command.
    [v] Arrow down      Choose a next command.
    [<] Arrow left      Move cursor to left.
    [>] Arrow right     Move cursor to right.
    Ctrl+[<]            Move cursor to left by word.
    Ctrl+[>]            Move cursor to right by word.
    [DEL]               Delete character on cursor.
    [BS]                Delete previous character.
    [TAB]               Move to the next tab stop, or auto-complete.

カーソルキーの左右でカーソル移動、Ctrl+左右で単語ごとに移動。上下キーで履歴を参照することができます。

ラインエディタ

基本的にラインエディタで入力を受け付け、入力したものを即時に実行します。

kinx[  0]> 10 * 2
=> 20

自動マルチライン判定と自動インデント

{ と対応する } を自動的に認識して、全体のブロックが入力し終わるまでエディタモードを継続します。この時、{ の数によって自動的に空白 4 つ分のインデントを付加します。

ただし、編集できるのはその行だけなので、[Enter] 入力後の編集はできません。

この機能を使って、functionclassmodule などを定義します。

kinx[  0]> function func(...a) {
    [  1]>     return a.reduce(&(r, e) => r + e);
    [  2]> }
=> function func

こうしておくことで、次のように関数を使えるようになります。

kinx[  0]> function func(...a) {
    [  1]>     return a.reduce(&(r, e) => r + e);
    [  2]> }
=> function func

kinx[  3]> func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
=> 55

登録した関数は以下のように確認できます。

kinx[  4]> .vars
function func(...a)

kinx[  5]> .showdef func
function func(...a) {
    return a.reduce(&(r, e) => r + e);
}

キーワード補完

コマンドやキーワードは [TAB] キーを押すことで補完モードになります。補完モードでは、先行する文字列に対して続く可能性のある文字列を薄いグレーで補完します。割と頑張って作ってみた上記のデモで確認できます。といっても上まで戻るのは面倒でしょうから、もう一回載せておきます(せっかく作ったので)。

この候補が表示された状態でさらに [TAB] キーを押すと、次の候補に切り替わります。この時、以下のキー操作になります。

  • 文字の入力 ... 一旦補完モードがキャンセルされ、先行する文字に入力した文字が追記されます。
  • 空白や記号の入力 ... 現在の候補で確定し、入力された文字が追記されます。
  • [BS][DEL] キー ... 現在の候補で確定し、カーソルはその位置で入力待ち状態になります。
  • [Enter] キー ... その場で確定させて、実行または次の行に移動させます。

補完はその位置のコンテキストで候補が変わります。例えば、過去の履歴から配列と推定される場合は Array のメソッドなども候補に追加されます。また、過去の履歴で new 演算子でインスタンス化したオブジェクトを代入した場合など、そのクラスメソッドなども候補に追加されます。

この実行例は最初に見せたデモで一部確認することが可能です。例えば 最後の行の s. の候補として valuesum が表示されるようになっています。

場合によってはうまく機能しないかもしれませんが、文字列を途中まで入力して [TAB] キーを押すことでその文字列で始まる候補が順々に出てくるので、いろいろと試してみてください。

あと、namespace は未対応です。

変数一覧

.vars コマンドで現時点で有効になっている変数の一覧が表示されます。尚、関数名やクラス名も変数名と同列の扱いなので、同じ名前で登録することはできません。後から定義したもので上書きされます。

kinx[ 20]> .vars
class Sample(...a)
a = 10
b = 20
c = 30
d = 40
e = 50
f = 60
g = 70
sample = new Sample(a, b, c, d, e, f, g)

変数の削除

.delete コマンドで不要になった変数を指定して削除できます。同じ変数名に代入すると上書きされるので、あまり使う機会はないかもしれません。ただ、上書きを繰り返して変数の登録順序がおかしくなったり、.delete コマンドを使ったことで変数の依存関係がおかしくなった場合に Compile Error となるケースもあります。そういったときにおかしくなった変数を削除し、新たに登録しなおすことで正しく動作させられるようになります。

例えば、上記の変数が登録された状態で以下のようになるケースがあります。

kinx[ 25]> .delete g      // g を削除
kinx[ 26]> sample.sum()   // g が無くなったので、sample インスタンスが不正な形に。
=> Error: Compile Error.

kinx[ 27]> .vars
class Sample(...a)
a = 10
b = 20
c = 30
d = 40
e = 50
f = 60
sample = new Sample(a, b, c, d, e, f, g)  // <- ここの g が無くなっているので不正な形。

kinx[ 28]> g = 60         // 新たに g を登録したいがコンパイルエラー。
=> Error: Compile Error.

kinx[ 29> .delete sample  // 不正な形のインスタンスが実行の邪魔をしているので、それも削除。
kinx[ 30]> g = 60         // g を再登録可能に。
=> 60

kinx[ 31]> var sample = new Sample(a, b, c, d, e, f, g)  // sample も再登録。
=> (null)

kinx[ 32]> sample.sum() // 新たな g を使って実行が可能に。
=> 270

関数、クラス、モジュール定義の表示

.vars では一覧表示の中に関数名やクラス名は表示されますが、その定義がどうだったかを表示はしません。そこで .showdef コマンドを使うと定義の中身を参照することができます。

kinx[ 33]> .showdef Sample
class Sample(...a) {
    @value = a;
    public sum() {
        return @value.reduce(&(r, e) => r + e);
    }
}

実行履歴

これまでの実行履歴を .history コマンドで確認することができます。例えば以下のような表示がされます(上記例とは番号が異なっていますが、サンプルです)。

[  0]: class Sample(...a) {
[  1]:     @value = a;
[  2]:     public sum() {
[  3]:         return @value.reduce(&(r, e) => r + e);
[  4]:     }
[  5]: }
[  6]: a = new Sample(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
[  7]: .vars
[  8]: System.println(a.sum());
[  9]: a = 10
[ 10]: b = 20
[ 11]: c = 30
[ 12]: d = 40
[ 13]: e = 50
[ 14]: f = 60
[ 15]: g = 70
[ 16]: .vars
[ 17]: var sample = new Sample(a, b, c, d, e, f, g)
[ 18]: sample.sum()
[ 19]: .vars
[ 20]: sample.sum()
[ 21]: .showdef System
[ 22]: .showdef Sample
[ 23]: .vars
[ 24]: sample.sum()
[ 25]: .delete g
[ 26]: sample.sum()
[ 27]: .vars
[ 28]: g = 60
[ 29]: g = 60
[ 30]: .vars
[ 31]: .delete sample
[ 32]: g = 60
[ 33]: var sample = new Sample(a, b, c, d, e, f, g)
[ 34]: sample.sum()
[ 35]: .history

過去の履歴から、以下のように ! を使って参照実行させることもできます。

kinx[ 36]> !34
=> "sample.sum();"
=> 270

また、カーソルキーの上下で履歴を再入力対象として選択することも可能です。

実行時間計測

.time on とすると、実行結果を表示するようになります。計測結果は単純に eval して返ってくるまでの経過時間なので色々なオーバーヘッドも含まれますが、目安にはなるでしょう。

kinx[  0]> native fib(n) {
    [  1]>     return n if (n < 3);
    [  2]>     return fib(n-2) + fib(n-1);
    [  3]> }
=> native<int> fib

kinx[  4]> .time on
.time: on

kinx[  5]> fib(39)
=> 102334155

elapsed:    1.238 s

kinx[  6]> fib(34)
=> 9227465

elapsed:    0.131 s

フル入力モード

.fullcode on とすると、[Enter] キーを押しただけでは実行しません。.run コマンドを入力するまでコマンド入力を継続します。ただし、今のところフル入力モードでは変数の登録やクラス定義の登録などが されません。全てその入力だけの単発の実行になります。

kinx[  0]> .fullcode on
.fullcode: on

kinx[  1]> class Something() {
    [  2]>         public println(...a) {
    [  3]>                 System.println(a);
    [  4]>         }
    [  5]> }
    [  6]> s = new Something();
    [  7]> s.println(1, 2, 3, 4, 5, 6, 7, 8);
    [  8]> .run
[1, 2, 3, 4, 5, 6, 7, 8]
=> {"s":null}

kinx[  9]> .vars

kinx[ 10]>

外部ファイル読み込み

外部ファイルを取り込むには .load コマンドを使います。通常ライブラリとして using するモジュールも .load で読み込みます。.load で読み込むことによって、ある程度のメソッド名補完を有効にすることができます。

例えば、DateTime を使う場合は以下のようにできます。

kinx[  0]> .load DateTime
=> Successfully loaded.

kinx[  1]> var dt = new DateTime(2020, 1, 1, 9, 30, 0)
=> (null)

kinx[  2]> .vars
dt = new DateTime(2020, 1, 1, 9, 30, 00)

kinx[  3]> dt.weekday()
=> 3

kinx[  4]> System.println(dt)
2020/01/01 09:30:00

検索ファイル名は 指定名.kx です。検索対象は以下の順に行われます。なので、標準ライブラリも参照できます。

  1. カレントディレクトリ
  2. kinx の実行ファイルのあるフォルダ配下の lib フォルダ内
  3. kinx の実行ファイルのあるフォルダ配下の lib/std フォルダ内
  4. kinx の実行ファイルのあるフォルダから見て ../lib フォルダ内
  5. kinx の実行ファイルのあるフォルダ配下の kinxlib フォルダ内
  6. kinx の実行ファイルのあるフォルダ配下の kinxlib/std フォルダ内

すみません、v0.9.2 リリースでは考慮漏れ(5 と 6 が Linux 用に必要だったが未実装なの)で Linux では .load DateTime がエラーします。ワークアラウンドとして .load /usr/bin/kinxlib/std/DateTime を指定してください。大変ですが。すみません。

ただし、DateTime に関しては現在 2 つほど注意点があります。将来のバージョンでは改善するかもしれません。

  • new 演算子を使わなくても DateTime オブジェクトを作れますが、メソッド名補完をするには new 演算子を使う必要があります。
  • new DateTime() で現在時刻を取得しますが、実行ごとに新たに new されるため、実行ごとに取得する値(時刻)が変化します。

East Asian Width

UTF8 での入力をサポートしています。East Asian Width の定義に従ってカーソル移動を行っているので、全角幅の文字はその幅で移動します。
以下の例では、あいうえお でも全角幅で 1 文字ずつ移動し、アイウエオ では半角幅でカーソルが移動します。また、[DEL] キーや [BS] キーでの削除も正しく動作します。

$ ./kinx --exec:repl
kinx[  0]> a = "あいうえおアイウエオ";
=> "あいうえおアイウエオ"

kinx[  1]> System.println(a);
あいうえおアイウエオ
=> (null)

kinx[  2]> a = "あいうおアイエオ";  // 「ウ」の上で [DEL] し、「お」の上で [BS] を押下。
=> "あいうおアイエオ"

kinx[  3]> System.println(a);
あいうおアイエオ
=> (null)

おわりに

なんかプログラミング言語は REPL が無いと半人前、みたいな意見をどこかで読んだので、頑張って作ってみました(大変だった...)。とは言っても、まだまだ発展途上。これを初版として、要望があれば改善対応していきたいと思います。

これを機会に触ってくれる人が増えると嬉しいかも。リリースビルドも公開しているので、もし宜しければ。

ではまた、次回。