コンテンツへスキップ
GitHubリポジトリ フォーラム RSSニュースフィード

Crystalのインタプリタ – とても特別なホリデープレゼント

ベータ・ジリアーニ

待望のCrystalインタプリタがマージされました。これを使用するには、特別なフラグを付けてCrystalをコンパイルする必要があります。執筆時点では、公式リリース(.deb、.rpm、dockerイメージなど)はコンパイルされていません。

この記事は、この特別な機能に関するFAQとしても機能します。最初から始めましょう。

なぜCrystalにインタプリタが必要なのか

多くの人がこれは当然のことと思うでしょうが、インタプリタが有効にする可能性のある2つの特定の機能について指摘することは有益です。

  1. 原則として、インタプリタはコード生成フェーズがスキップされるため(下記参照)、コードの実行をより速く開始し、再コンパイルを必要とせずにコードの結果をすばやくテストできるようになります。インタプリタは一度だけ実行する必要がある短いプログラムには高速ですが、コンパイルモードははるかに高いパフォーマンスを提供し、ほとんどの本番環境でのユースケースに適していることに注意する必要があります。また、インタプリタ型プログラムとコンパイル型プログラムの動作は一部の側面(システムによる)で異なる可能性がありますが、違いを最小限に抑えることを目指しており、ほとんどのプログラムはインタプリタモードとコンパイルモードでまったく同じように動作するはずです。
  2. インタプリタは、言語固有のツールであるため(汎用的なlldbとは異なり)、デバッグエクスペリエンスを向上させます。

現在のステータスは?

インタプリタは現在、実験段階にあり、多くの欠けている部分があります。この初期段階でマージした理由は、インタプリタ関連のPRに関する適切な議論を可能にし、開発を少しスピードアップするためです。試してみる意思のある方には、前述の既知の問題を考慮して、インタプリタ関連の問題を歓迎します。

どのように動作するのですか?

元のPRがそれに答えています。

インタプリタモードで実行する場合、意味解析は通常どおり行われますが、その後LLVMを使用してコードを生成する代わりに、コードをバイトコード(このPRで定義されたカスタムバイトコードであり、LLVMとは完全に無関係)にコンパイルします。次に、このバイトコードを理解するインタプリタがあります。

どのように起動するのですか?

⚠️ これは確定していません!

makeにフラグinterpreter=1を渡してCrystalをコンパイルしたと仮定すると、現時点では2つのモードを使用してインタプリタを起動できます。

  • crystal i file.cr

このコマンドはファイルをインタプリタモードで実行します。したがって、write_hello.crに以下が含まれている場合

File.write("/tmp/hello", "Hello from the interpreter!")
puts "done"

crystal i write_hello.crを起動すると、ファイル/tmp/helloが生成され、stdout"done"が出力されます。

  • crystal i

このコマンドは、Rubyのirbに似た対話型Crystalセッション(REPL)を開きます。このセッションでは、コマンドを記述してその結果を取得できます。

icr:1:0> File.read_lines("/tmp/hello")
=> ["Hello from the interpreter!"]

(注:強調表示はまだインタプリタの一部ではありません。)

プログラムのデバッグ

これら2つのモードのいずれでも、コード内のdebuggerを使用して、その時点でデバッグできます。これはRubyのpryに似ています。そこでは、これらのコマンド(pryにも似ています)を使用できます。

  • step:メソッド内に入る可能性のある、次の行/命令に進みます。
  • next:メソッド内に入らない、次の行/命令に進みます。
  • finish:現在のメソッドを終了します。
  • continue:実行を再開します。
  • whereami:デバッガの位置を表示します。

たとえば、write_hello.crwriteputsの間にdebuggerを追加した場合、ファイルをインタープリタした後、次のようになります。

From: /tmp/write_hello.cr:3:6 <Program>#/tmp/write_hello.cr:

    1: File.write("/tmp/hello", "Hello from the interpreter!")
    2: debugger
 => 3: puts "done"

pry>

そして、stepを実行すると、標準ライブラリからのメソッドが呼び出されていることがわかります。

pry> step
From: /Users/beta/projects/crystal/crystal/src/kernel.cr:385:1 <Program>#puts:

    380:
    381: # Prints *objects* to `STDOUT`, each followed by a newline character unless
    382: # the object is a `String` and already ends with a newline.
    383: #
    384: # See also: `IO#puts`.
 => 385: def puts(*objects) : Nil
    386:   STDOUT.puts *objects
    387: end
    388:
    389: # Inspects *object* to `STDOUT` followed
    390: # by a newline. Returns *object*.

pry>

この時点で、私たちは興味を持つかもしれません。このメソッドに渡したobjectsは何ですか?もう一度stepを実行して、変数をスコープに含め、次を発行します。

pry> objects
{"done"}

インタプリタはプログラムのロードをどれくらい高速化しますか?

特に、多くのシャードが正常にインタプリタとして解釈できないことを考えると、ベンチマークが不足していることは間違いありません。標準ライブラリとCrystal Patternsのいくつかのランダムなファイルでテストすると、それらのロード(および実行、下記参照)は、コンパイルする場合よりも50〜75%高速です(time crystal <file>time crystal i <file>を比較)。これは、構文解析や意味解析など、コンパイラとインタプリタの共通ステップにCrystalが費やす時間に大きく依存します。

どのくらいの速さ(または遅さ)で実行されますか?

作成者のAryが、示したように、Crystal Conf 1.0では、十分な速さで実行されています。もちろん、Rubyのような成熟したインタプリタの実装ほど効率的ではありませんが、予備テストでは、予想されるユースケースには十分に高速に実行されます。たとえば、1秒以内に数百万の整数加算を処理できます。とは言うものの、集中的なタスクの処理にインタプリタを使用することは、コンパイルされたプログラムのタスクであるため、明確に推奨されません。


結論として、PRのマージは、Crystalで動作するインタプリタを取得するための最初のステップであり、さらに重要なことに、開発者エクスペリエンスを向上させるというCrystalチームの意志の証です。