LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-23

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

2
1
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
2
1