Edited at
CrystalDay 23

Crystal本体のコードを眺める コンパイルの下準備編

この記事は Crystal Advent Calendar 2018 の23日目の記事です。


はじめに

Crystalの日本コミュニティでは主にmakenowjustさんがCrystal本体

https://github.com/crystal-lang/crystal

に多大にコントリビュートされてます。

https://github.com/crystal-lang/crystal/graphs/contributors

を見ると4番目

makenowさん曰く「Crystalの型推論には無限のバグがありそうな気がする」とのことなので、私もそろそろコンパイラ本体は見ておきたい感がしてきました。

後少しでも盛り上げないといよいよ寂れそうで

この記事はCrystal本体のソースのコードリーディングの記録をまとめます。

22日にarcageさんの記事でも触れていますが、そちらが公式発の情報ですので、確かな情報を知りたい場合はそちらをお勧めします。

本記事は自力で頭からコードを読んでいこうというものですので。

何かしらの成果があるとは限りませんし、時間いっぱい(23日の日付が変わるまで)の制限付きです。

また筆者はコンパイラ未経験者ですので、間違い等もあると思いますがそれを鑑みて以下をご覧ください。(ご指摘歓迎)


主な構成

まずエンドポイントはどこなのかを探していきます。

手っ取り早いのがカレントにあるMakefileを見てみます。

$(O)/crystal: $(DEPS) $(SOURCES)

@mkdir -p $(O)
$(BUILD_PATH) $(EXPORTS) ./bin/crystal build $(FLAGS) -o $@ src/compiler/crystal.cr -D without_openssl -D without_zlib

とあり、src/compiler/crystal.crを見に行っています。

ではsrc/compiler/crystal.crを見てみます。

# This is the file that is compiled to generate the

# executable for the compiler.

{% raise("Please use `make crystal` to build the compiler, or set the i_know_what_im_doing flag if you know what you're doing") unless env("CRYSTAL_HAS_WRAPPER") || flag?("i_know_what_im_doing") %}

require "./crystal/**"

Crystal::Command.run

src/compiler直下のcrystalディレクトリ以下のファイルをロードした上でCrystal::Command.runを実行しています。

一旦Command.runの方を見ていきます。

class Crystal::Command

(ry)
def self.run(options = ARGV)
new(options).run
end

  def run

command = options.first?
case
when !command
puts USAGE
exit
(以下条件分岐)

build以外にはinit(テンプレート生成する)やplay(playground立ち上げ)、run(実行ファイルを生成しないでそのまま実行)など。

    when "build".starts_with?(command)

options.shift
build

buildメソッドを見ます。

  private def build

config = create_compiler "build"
config.compile
end

create_compilerの中身を見てみます。

  private def create_compiler(command, no_codegen = false, run = false,

hierarchy = false, cursor_command = false,
single_file = false)

OptionParser.parse(options) do |opts|

で延々とオプションを解釈し、コンパイラへと設定を追加していっています。

@config = CompilerConfig.new compiler, sources, output_filename, original_output_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format

最後にCompilerConfigに上記の設定を追加してインスタンスを生成しています。

上記を踏まえた上でもう一度buildメソッドに戻ります。

  private def build

config = create_compiler "build"
config.compile
end

compileコマンドを追ってみます。

  record CompilerConfig,

compiler : Compiler,
(ry)
def compile(output_filename = self.output_filename)
compiler.emit_base_filename = original_output_filename
compiler.compile sources, output_filename
end

def top_level_semantic
compiler.top_level_semantic sources
end
end

コンパイルの実行箇所は

src/compiler/crystal/compiler.cr にあります。

module Crystal

class Compiler
def compile(source : Source | Array(Source), output_filename : String) : Result
source = [source] unless source.is_a?(Array)
program = new_program(source)
node = parse program, source
node = program.semantic node, cleanup: !no_cleanup?
result = codegen program, node, source, output_filename unless @no_codegen

@progress_tracker.clear
print_macro_run_stats(program)
print_codegen_stats(result)

Result.new program, node
end

new_programメソッドを見てみます。

  private def new_program(sources)

program = Program.new
program.filename = sources.first.filename
program.cache_dir = CacheDir.instance.directory_for(sources)
program.target_machine = target_machine
program.flags << "release" if release?
program.flags << "debug" unless debug.none?
program.flags << "static" if static?
program.flags.concat @flags
(ry)
program
end

対象となるコードの情報とコンパイルのフラグを見ているようです。

もう一度compileメソッドに戻ります。

node = parse program, source

parseを追ってみます。

    private def parse(program, sources : Array)

@progress_tracker.stage("Parse") do
nodes = sources.map do |source|
# We add the source to the list of required file,
# so it can't be required again
program.add_to_requires source.filename
parse(program, source).as(ASTNode)
end
nodes = Expressions.from(nodes)

# Prepend the prelude to the parsed program
location = Location.new(program.filename, 1, 1)
nodes = Expressions.new([Require.new(prelude).at(location), nodes] of ASTNode)

# And normalize
program.normalize(nodes)
end
end

個別のファイルを解析している所を見ます。

parse(program, source).as(ASTNode)

同じ名前のメソッドですけど、第2引数がSource型の方です。(Crystalはオーバーロード対応)

    private def parse(program, source : Source)

parser = Parser.new(source.code, program.string_pool)
parser.filename = source.filename
parser.wants_doc = wants_doc?
parser.parse
rescue ex : InvalidByteSequenceError
stderr.print colorize("Error: ").red.bold
stderr.print colorize("file '#{Crystal.relative_filename(source.filename)}' is not a valid Crystal source file: ").bold
stderr.puts ex.message
exit 1
end

ここでいよいよコンパイラの肝の部分である解析を行い箇所に到達しました。

parser.parse

中身はcompiler/crystal/syntax/parser.crにあります。

と、今日はここまでにします。

parserの中身については25日に続きを書きます。