LoginSignup
10
7

More than 5 years have passed since last update.

REPLのつくりかた (Node篇)

Last updated at Posted at 2018-11-11

言語を自作していると、特殊用途でない限り CLI は概ね実装することになりますが、 REPL(Read-Eval-Print Loop; 対話的にコードを実行できる仕組み) は必ずしも要らないため、後回しにしてしまいがちです。

なぜ、後回しにしてしまうかを説明したいと思います。

1. だるい

便利に使えるREPLというのは、メジャーな言語のREPL(Python, JavaScript, Ruby, その他いろいろ)を見ればわかりますが、複数行にわたるコンソールの編集機能を提供していたり、入力のヒストリー機能を持っていたりします。
これらは、単純に標準入出力を読み書きするだけでは実現できません。
端末の行・列のサイズを取ったり、ユーザーのキー入力(矢印キーやバックスペース等)で入力カーソルを動かしたり、文字を消したりする必要があります。

2. だるい

端末の画面描画を行うためには、端末にエスケープシーケンスによるコントロールコードを送信します。
ANSIエスケープシーケンス チートシート

3. だるい

ただ、いくら何でも毎度エスケープシーケンスを送るコードを書いていては、訳が分からなくなってくるので、一般的に端末の画面制御は「端末制御ライブラリ」と呼ばれるものを使用します。
Cのライブラリとしては、ncurses 等があります。

4. だるい

画面描画ができたとしても、カーソル位置や入力の制御も必要です!
面倒だ!! CLIから読ませればいいじゃない!!!

REPLをつくろう

しかし、先人たちもこのような面倒なことを放置するわけもなく、シェルやREPLのためのテキスト入出力ライブラリが作られました。
もっとも有名なものが GNU Readline です。GNU Readline は bash にも使われています。

Nodeにも以前より(GNU Readlineとは少し異なるものを含め) Readline 的機能を実現するライブラリがありましたが、Node.js 10.0 から標準モジュールに含まれるようになりました。
わずか 100行でかなりしっかりした REPL が実装できました。

readline 😍👍 、 console.dir 😍👍 !

// Node.js>=10.0
// 旧バージョンでの代替手段として linebyline (https://www.npmjs.com/package/readline) があります。
//   ※互換はありません。
const readline = require('readline');

// 構文解析の結果、ステートメントが完了していないことを示すエラー。
class ScriptUnexpectedTerminationError extends Error {
}

// オレオレ言語の 字句解析器(lexer) + 構文解析器(parser)。
function parse(s) {
    s = (s || '').trim();
    if (s.length === 0) {
        throw new ScriptUnexpectedTerminationError('Unexpected termination of script');
    }

    let m;
    if (! s.startsWith('(')) {
        throw new Error('Syntax error');
    }
    if (m = s.match(/^\(([^]*)\)$/)) {
        return m[1].split(/\s/);
    }
    throw new ScriptUnexpectedTerminationError('Unexpected termination of script');
}

// オレオレ言語のインタープリタを作成する関数。
//    状態を内包している必要があります。
function createEvaluater() {
    const ctx = { /* your interpreter's internal status */ };
    return ((s) => {
        return parse(s).map(x => x + '!');
    });
}

// REPLを開始します。
function startRepl(startup) {
    const prompt = '>>> ';
    let pkgName = 'OreOreGengo';
    let pkgVer = '0.0.0';

    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        prompt,
    });

    const repl = createEvaluater();

    // ユーザー入力の前に実行したいコードがあれば受け付ける
    if (startup && startup.trim().length > 0) {
        console.dir(repl(startup));
    }

    console.log(`${pkgName} ${pkgVer} CLI (REPL)\nType ^C to exit.\n`);
    rl.prompt();

    let buf = '';

    // 行の入力を受け付ける
    rl.on('line', (input) => {
        buf += input;
        const trimmed = buf.trim();

        if (trimmed.length) {
            try {
                parse(buf);
            } catch (e0) {
                if (e0 instanceof ScriptUnexpectedTerminationError) {
                    // 複数行の入力
                    buf += '\n';
                    rl.setPrompt('... ');
                    rl.prompt();
                    return;
                }
            }

            try {
                console.dir(repl(buf));
            } catch (e) {
                console.error(e);
            } finally {
                buf = '';
            }
        } else {
            buf = '';
        }

        rl.setPrompt(prompt);
        rl.prompt();
    })
    .on('close', () => {
        console.log('Bye!');
        process.exit(0);
    });
}

startRepl('(1 2 3 5 7 11 13)');

実行結果

[ '1!', '2!', '3!', '5!', '7!', '11!', '13!' ]
OreOreGengo 0.0.0 CLI (REPL)
Type ^C to exit.

>>> (
... 1 2 3
... )
[ '!', '1!', '2!', '3!', '!' ]
>>> Bye!

さいごに

拙作のLisp処理系 LiyadCLI+REPL をリリースしました。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7