この記事は 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日に続きを書きます。