ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Clean Code 読書会のすすめ

ESM アドベントカレンダー2023の17日目の記事です。

adventar.org

こんにちは haruguchiです。

この記事では社内で行なっている Clean Code 読書会の紹介と1年以上継続して良かったことを書きたいと思います。

Clean Codeについて

ボブおじさん(uncle bob)ことRobert C. Martinの著書で、クリーンで洗練されたコードを書くための手がかりをいろんな観点で示してくれる本です。

手がかりと書いたのはこの本の序論には以下のような文章があるからです。

クリーンコードを書くことを身につけるには努力が必要です。原則とかパターンといった知識を身につけるだけではだめなのです。実際に汗をかかなければならないのです。自分自身で実践してみて、そして失敗してみなければならないのです。また、他の人が実践しているのを、また失敗しているのを見なければならないのです。彼らがつまづき、やり直すのを観察しなければならないのです。彼らが判断に苦しむのを見て、間違った判断の結果として、彼らが支払うはめになった代償を見なければならないのです。 (序論より)

このように知識として知っていれば良いだけでなく自分で考え、または人と議論しクリーンなコードを書く訓練をする本なので読書会でディスカッションするのには最適だと考えています。

読書会の概要と進め方

この読書会は週1回1時間という枠でプロジェクトを横断して参加できるようになっています。また、特定の主催者をたてていないので、誰かが欠席しても人が集まれば自動的に開催されます。暗黙的なルールとして3人以上集まれば開催されることが多いです。

進め方は以下の通りです。

  1. 司会をたてる
  2. 読むところを決め黙読
  3. 読んだセクションの感想、意見、質問なんでも声に出す(ディスカッションパート)
  4. 時間の限り 2. 3.を繰り返す
  5. クロージング
  6. 終わった後はesaに感想を(任意)

司会の仕事はesaのページを作成するのと、1回の読書で読む範囲を決めることくらいなので誰でも気軽にできます。esaのページには見出しとして「参加メンバー一覧」、「読んだところ」、「次回読むことろ」などがありシンプルな作りになっています。

各自読み終わったらslackのスタンプで報告します。メンバー全員が読み終わったらディスカッションパートへ移行します。

ディスカッションといっても、そのセクションに対しての感想からわからなかったことまで気軽に発言していきみんなで理解を深めていきます。サンプルコードがJavaで書かれているということもあって、Java特有の知識に関してはJava経験者に教えてもらうことが多いです。

終わった後は各自任意でesaのコメント欄に感想を記入します。

esa記事のコメント欄に感想を記入している
読書会後の感想記入

読書会に1年以上参加して

Clean Code 読書会に参加して良かったことは次の3つです。

ディスカッションすることでメンバーの書いたコードへの理解が深まった

たとえば、クラスの中にメソッドをどの順番で定義するのか?という話題があります。本書では新聞の見出しのように抽象度の高いものから順に定義していく方法と、呼び出される順番に定義していく方法の2通りが出てきました。

# 新聞の見出しのように定義
class Foo
  def main
    method_a
    method_b
  end

  def method_a = method_c

  def method_b = method_d

  def method_c; end

  def method_d; end
end


# 呼び出す順に定義
class Foo
  def main
    method_a
    method_b
  end

  def method_a = method_c

  def method_c; end

  def method_b = method_d

  def method_d; end
end

このことでディスカッションした結果、「こういう時はこっちの方が良いんじゃないかな」「僕は今までこの方法でやってましたね」「プロジェクトでは〜」「一貫して順番が決まってることが大事だよね」といった議論をすることで、コードが意図を持って書かれているということを意識するようになりました。

Ruby や JavaScript 以外の言語の本を読むことへの抵抗感がなくなった

本書はサンプルコードはJavaなのでRubyとJavaScriptしか理解していない私にとって最初はかなりとっつきにくいものでした。読書会では同じようなメンバーもたくさんいますがみんな工夫してエッセンスを抽出して理解していることに気づきました。サンプルコードはあくまでも、そのセクションで主張したいことに説得力を持たせるための例になっていることが多いので細かいコードを追うのではなく、どんなことをしているコードなのか全体像を掴むことが大切だと感じました。

他にもChat GPTにコードを解説させたり、リファレンスを読んだりと未知なる言語の本を読む知見がたまったのもこの読書会のおかげだと思っています。

興味関心の枝葉が広がった

Clean Codeの目次を見るとわかるように扱っている題材が広範に及びます。そのため、読書会の後、復習として気になるキーワードを拾っていくだけでも大変勉強になりました。 たとえば、デザインパターンの章ではGoFのデザインパターンが紹介されますが、あまり詳しく知らなかったこともあって結城浩さんのJava言語で学ぶデザインパターン入門第3版という本を購入し学習するきっかけとなりました。

www.hyuki.com

他にも、並列処理やアスペクト志向プログラミングなど以前の自分に馴染みのなかった分野に対して興味を持てたことが収穫でした。

最後に

私たちは日々クリーンなコードとは何か考えてプロジェクトと向き合っています。弊社の読書会が何らかの参考になれば幸いです。


株式会社 永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Railsのsystem testにおいて実行行ごとにhookを追加するgemを作った

この記事は ESM アドベントカレンダー 2023 の9日目です。

こんにちは @color_boxです。

今回は system_test_step_hook というgemを作ったので、それについて書きます。

rubygems.org

こちらのgemはRailsアプリのsystem testで動作します。 テストコードの各行に対して、RSpecのbefore/after hookのように、何らかの処理の追加を可能にします。

使い方は簡単で、このgemをインストールして

SystemTestStepHook.before do
  pp 'this is before hook'
end

SystemTestStepHook.after do
  pp 'this is after hook'
end

のように追記すると、systemテストの各行に対して、before/after hookのようなタイミングで追記したコードが実行されます。

本記事では、こちらのgemを仕組みについて軽く解説できればと思います。

仕組み

そもそもsystem testにおいて、コード一行ずつの実行を検知するような仕組みはありません。

ですので、動的にテストコードを書き換えて実現します。

  1. system test実行時にテストコードが書かれたブロックを取り出す
  2. 取り出したコードブロックをParserで分解し、各行の前後で渡されたブロックを実行するコードを挿入
  3. 編集したコードをevalで実行し、結果を得る

という流れになります。

取り出したコードブロックを実際に編集している箇所が下記で https://github.com/colorbox/system_test_step_hook/blob/main/lib/system_test_step_hook.rb#L17-L35

編集したコードブロックをevalで実行して結果を得ているのが下記です。 https://github.com/colorbox/system_test_step_hook/blob/main/lib/system_test_step_hook.rb#L37-L68

Minitest::Testモジュールにモンキーパッチを当てて対応しています。

SystemTestStepHookのモジュール内で編集したコードをevalしてしまうと、編集前のコードと実行コンテキストが変わってしまうため、eval自体をMinitestのコンテキストで呼び出すようにしています。

目的

なぜこのようなgemを作ったかについてですが、それは下記のgemに組み込むための部品とするためです。

github.com

こちらのgemは、system testを実行するとスクリーンショットとテストコードを組み合わせてHTMLを作成し、それを仕様書として使えるようにするというgemです。 元々system testの行ごとに何らかの処理を行うという挙動はこちらのgemのものだったのですが、そのhookの部分のみを抽出した形になります。 単機能で切り出せばその分応用も効きますし、保守性が上がるというわけですね。


株式会社 永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2023年12月のお知らせ

2023年12月の Rails / OSS パッチ会を 12月11日(月)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

今月リリース予定の Ruby 3.3 などに関する話題があるかもしれません。

弊社の構文解析器研究部に関心がある方や、これからパッチ会に参加してみたいという方、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

ESM アジャイル事業部 カジュアル面談での質問集

nsgc です。この記事は、ESM Advent Calendar 2023 - Adventarの7日目の記事です。

私は普段エンジニアをしていますが、カジュアル面談で会社や事業部についてお話をさせていただくこともちらほらあります。 そんな話し合いの場で、頂いた質問と回答を幾つかご紹介しようと思います。

私たちは受託開発という業務内容から、具体的にどういう会社なのかイメージが湧かないこともあるかと思いますので、 ぜひご参考になればと思います。

Q. お客さんとはどのように開発に関わるのか?

私たちが参加するプロジェクトでは、お客様も含めて 1 つのチームとして開発することが多いです。

お客様のエンジニアと一緒に設計をしたり、プログラミングをしたり、コードレビューを行ないます。 朝会、イテレーションでの計画やふりかえりといった活動も一緒に行ないます。

プロダクトの責任者(プロダクトオーナー)と会話しながら、プロダクトをより良い方向に導くお手伝いをすることもあります。 言われた通りに作るのではなく、やりたいことをベースにこちらから提案をしたり、お客様の課題を解決するために、一緒に考えることも多いです。

Q. 登壇しているのをよく見ますが、登壇するのは強制なのでしょうか?

RubyKaigi などで登壇しているメンバーもいるのでよく聞かれますが登壇は強制やノルマはありません。エンジニアコミュニティと関わるのが好きなメンバーが多いという点は挙げられそうです。

加えて、エンジニアリングマネージャーとの 1on1 でコミュニティ活動についての相談をしたり、プロポーザルを考える会(CFPを考える会)などを開催し、興味のあるメンバーや登壇したいメンバーの後押しやサポートをする仕組みがあるので、発表する機会は多いのかもしれません。

Q. 長いプロジェクトはどれくらい?その Railsバージョンは?

一番長くお付き合いのあるプロダクトは、2011年から現在も続いており12年目になります。 Rails 3.0.3 から始まっており、先月アップグレードし 7.1.2 になりました。 Ruby は 3.2.0 です。

多くのプロジェクトでは、Ruby や Rails は、なるべく最新の状態を追随するようアップグレードしております。 セキュリティフィックスやバグフィックスを早急に取り込めるのと、新しい機能を速く使えるようにするためです。

Q. フロントエンドについて「React 推し」「Vue 推し」「Hotwire 推し」とかありますか?

バックエンドは、ほぼ Rails を用いているサービスに関わりますが、プロジェクトによってはフロントエンドを触る機会も多いです。

ただし、アジャイル事業部として、どれかを推しているというのは特に無く、そのチーム毎にプロジェクトの状況やメンバーでの話し合いのもと選択しています。

現在は React を採用しているチームが多いです。

Q. 年齢を重ねても現場でコードを書き続けることはできるのか?

キャリアとして、年をとったらマネジメントを専門にする役割(いわゆるマネージャー)にならないといけないということはありません。 エンジニアとしてあり続ける事ができ、プログラミングをし続ける事ができます。

ただし、ベテランには、若手、後輩の育成や事業部の持つ課題への取り組みなども求められます。

さいごに

株式会社永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと共生しながら成長したいエンジニアを絶賛募集しています。 ブログの内容で私たちに興味を持った方、ぜひこちらからどうぞ。

agile.esm.co.jp

Rubyコミュニティとつながっていくための事業部施策の紹介(2023年12月現在)

この記事は、ESM Advent Calendar 2023 - Adventarの6日目の記事です。

こんにちは、平田です。11月に開催された RubyWorld Conference 2023 に参加されたみなさん、楽しかったですね!ESMでは、スポンサープレゼンテーションとして「ESMスーパーライトニングトークス by ESMスーパーライトニングトーカーズ」というものを発表させていただきました。お昼休みの時間帯にも関わらず、多くの方に応援にきていただき、大変うれしかったです。

現地で見ていただいた方はお分かりいただけるかと思いますが、1人80秒という時間制約の中でメンバーそれぞれが話したいことを話す場でした。

私は「Ruby x Agile グループのマネージャを10年やってみた」というタイトルで話をしたのですが、当然80秒に収まらなかったので、その内容についてこのブログで紹介したいと思います。 一番お見せしたかったスライドがこれです。

施策一覧

アジャイルなソフトウェア開発を生業にしている私たちは、自分たちの組織もプロダクトだと考えて、毎月新しい施策をリリースするということをこの2年ほど続けてきました。コロナ禍による働き方の変化への対応や、コミュニティと共生すること、個人やチームの成長につながる施策などを今でも毎月リリースしています。

今回の発表のために取り組んでいる施策をあげていくと、活用頻度が下がってきている施策もあったりして、止めるものは止めてしまってもいいかな?という気づきもありましたが、新陳代謝をあげて今必要な施策に取り組んでいきたいと思いました。

施策名だけだと何をやっているか分からないものや、内容の想像はつくんだけど具体的にどう運用しているのか等、分からないことや一家言あります!という方もいるかと思いますが、コミュニティイベント等で永和メンバーと会ったときにでも聞いてください!

また、施策の内容について興味がある方はカジュアル面談の場でお話させていただくことも大歓迎です。 永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したい、そしてその環境を一緒に作っていきたいエンジニアを絶賛募集しています。

agile.esm.co.jp

ESM アジャイル事業部が購入している書籍たち (2023年版)

この記事は ESM アドベントカレンダー 2023 の5日目です。

以前公開した『ESM アジャイル事業部が購入している書籍たち (2022 秋) 』からおよそ1年ほど経過しました。

blog.agile.esm.co.jp

その後の1年間、永和システムマネジメント アジャイル事業部で運用している書籍購入支援制度で購入されている書籍をこの記事でリストアップします。2023 年に最も購入された書籍となる ESM Bookshelf of the Year 2023 はどの一冊でしょう?

弊社事業部メンバーたちがどのような書籍を購入しているか、年末年始の読書の参考にどうぞ。

★は複数人が購入。★★はその中でも購入が多かったタイトルです。

Ruby

Ruby に関する書籍では、@kakutani フェロー最新訳の『研鑽Rubyプログラミング』が、昨年の『研鑽Rubyプログラミング β版』に続き最も購入されました。今月リリース予定の Ruby 3.3 とあわせて年末年始に楽しんでみても良さそうです。

  • 研鑽Rubyプログラミング ★
  • Programming Ruby 3.2
  • リファクタリング:Rubyエディション
  • Agile Web Development with Rails 7

JavaScript / TypeScript

JavaScript 関連の書籍はばらつきが多い傾向にありました。

  • JavaScript Primer 迷わないための入門書
  • 初めてのTypeScript―型安全なJavaScriptでWeb開発を加速する
  • プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで
  • Node.jsデザインパターン 第2版
  • Reactハンズオンラーニング 第2版
  • The Road to React
  • Real-World Next.js

Web

  • Webアプリケーションアクセシビリティ──今日から始める現場からの改善 (WEB+DB PRESS plus)
  • 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版[リフロー版]
  • Web API: The Good Parts
  • OAuth徹底入門 セキュアな認可システムを適用するための原則と実践

データベース

  • MySQL徹底入門 第4版 MySQL 8.0対応
  • [改訂3版]内部構造から学ぶPostgreSQL ―設計・運用計画の鉄則
  • 達人に学ぶSQL徹底指南書
  • SQL実践入門 ──高速でわかりやすいクエリの書き方

クラウド

  • AWSコンテナ設計・構築[本格]入門

ソフトウェア設計

現在の読書会でテーマにしている『Clean Code アジャイルソフトウェア達人の技』の購入が多かったです。その読書会からの派生で書籍購入されることもあります。

  • Clean Code アジャイルソフトウェア達人の技 ★★
  • Clean Architecture 達人に学ぶソフトウェアの構造と設計
  • リファクタリング(第2版): 既存のコードを安全に改善する
  • Java言語で学ぶデザインパターン 第3版
  • ソフトウェア設計のトレードオフと誤り
  • マイクロサービスアーキテクチャ第2版
  • モノリスからマイクロサービスへ
  • ソフトウェアアーキテクチャ・ハードパーツ
  • データ指向アプリケーションデザイン

Linux

  • [試して理解]Linuxのしくみ ―実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識【増補改訂版】 ★
  • 本気で学ぶ Linux実践入門 サーバ運用のための業務レベル管理術

処理系

構文解析器研究部の流れからか『Rubyを256倍使うための本 無道編』の購入が度々ありました。2001年2月の出版ということで、Ruby パーサー界において「歴史の風雪に耐えた名著」の一冊と言って良いでしょう。

  • Rubyを256倍使うための本 無道編 ★
  • RubyでつくるRuby ゼロから学びなおすプログラミング言語入門
  • ガベージコレクションのアルゴリズムと実装
  • プログラマーのためのCPU入門 ― CPUは如何にしてソフトウェアを高速に実行するか

そのほか

この一年は全体として、購入書籍の重複は前年ほどなく、それぞれ自分が必要とする課題感にあわせた選出が色濃く出ていたようです。そのほかの購入書籍から抜粋します。

  • マスタリングTCP/IP入門編 ★
  • エンジニアリングマネージャーのしごと
  • スタッフエンジニア マネジメントを超えるリーダーシップ
  • ソフトウェア見積り 人月の暗黙知を解き明かす
  • 熊とワルツを リスクを愉しむプロジェクト管理
  • ユーザーストーリーマッピング
  • ソフトウェアテスト293の鉄則
  • 並行プログラミング入門
  • 推薦システム実践入門 ―仕事で使える導入ガイド
  • 入門 監視 ―モダンなモニタリングのためのデザインパターン
  • 問題解決力を鍛える!アルゴリズムとデータ構造
  • OpenAI GPT-4/ChatGPT/LangChain 人工知能プログラミング実践入門
  • n月刊ラムダノート 既刊5冊セット

「ESM Bookshelf of the Year 2023」🥇

最も購入されていた書籍は、前述したとおり社内読書会で使われている『Clean Code アジャイルソフトウェア達人の技』でした。読書会に取り上げられると購入数が一気に上がりますね。

このように読書会が開催される書籍や、現場で直面している書籍などは制度の活用対象で良く選ばれています。興味のある書籍があれば手に取ってみると新しい発見があるかもしれません。


株式会社 永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rubyでつくる、ミニでRubyなコンパイラ

こんパイラ〜(挨拶)、電子の海に漂うはかなき泡沫(うたかた)、はたけやまです。

みなさん、書籍「RubyでつくるRuby」をご存知ですか?Rubyを使ってRubyのサブセット「MinRuby」のインタプリタを作ることで言語処理系作成のエッセンスを学ぼう!という本です。

今回は、この本のMinRubyを題材に、簡易なMinRubyコンパイラをRubyで作成してみようと思います。

(この記事は ESM Advent Calendar 2023 の4日目の記事になります)

Gitリポジトリ

今回作成したコンパイラのソースはこちらのgitリポジトリに置いてます。

https://github.com/thata/minrubyc-m1

言語仕様とターゲット環境

  • 言語仕様
    • MinRubyのサブセット
      • データ型はInt(整数)のみ
      • ArrayとHashは実装しない
      • 関数の引数は8つまで
      • (それ以外にも足りてない機能が山ほどあるよ)
  • ターゲット環境
    • ターゲットOS
      • macOS
    • ターゲットCPU
      • Apple Mシリーズ(M1, M2 など)

インテルのCPUで動かせるの?

今回作成するコンパイラはApple M1向けのコードを生成するため、インテルCPUのマシンでは動作させることができません。代わりに、今回のコンパイラをインテルCPUへ移植したものがあるのでそちらを見てみてください。

字句解析器と構文解析器

コンパイラに欠かすことのできない「字句解析」と「構文解析」と呼ばれる処理があります。

  • 字句解析
    • プログラムを「単語」や「記号」などの「トークン」と呼ばれるプログラムの最小単位に分割する処理
  • 構文解析
    • 文法を元にトークンを組み合わせて構文木を構築する処理

通常のコンパイラでは字句解析を行う字句解析器と構文解析を行う構文解析器を自前で実装する必要がありますが、今回作成するコンパイラでは「RubyでつくるRuby」使われているMinRubyのパーサをそのまま利用します。

MinRubyのパーサはRubyGemsとして提供されているので、事前に以下のコマンドを実行して minruby gemをインストールしておいてください。

gem install minruby

MinRubyパーサの使い方

MinRubyのパーサの使い方はこんな感じ。渡されたソースコードを構文木の形式に変換して返します。

irb(main):001:0> require 'minruby'
=> true
irb(main):002:0> minruby_parse "10"
=> ["lit", 10]
irb(main):003:0> minruby_parse "10 + 20"
=> ["+", ["lit", 10], ["lit", 20]]

MinRubyパーサが返す構文木のノードには主に以下のようなものがあります。

  • リテラルノード
    • ["lit", 10]
  • 四則演算ノード( + - * /
    • ["+", ["lit", 10], ["lit", 20]]
    • ["-", ["lit", 10], ["lit", 20]]
  • 変数代入ノード
    • ["var_assign", "a", ["lit", 10]]
  • 変数参照ノード
    • ["var_ref", "a"]
  • ステートメント(複文)ノード
    • ["stmts", ["var_assign", "a", ["lit", 10]], ["func_call", "p", ["var_ref", "a"]]]
  • 比較演算子ノード( > < >= <= == !=
    • ["==", ["lit", 1], ["lit", 1]]
  • if ノード
    • ["if", ["==", ["lit", 1], ["lit", 1]], ["stmts", ["lit", nil], ["lit", "foo"]], ["stmts", ["lit", nil], ["lit", "bar"]]]
  • 関数定義ノード
    • ["func_def", "foo", [], ["lit", 0]]
  • 関数コールノード
    • ["func_call", "p", ["lit", 10]]

コンパイラのはじめの一歩

では、コンパイラを作っていきます。まずは整数リテラルを表示するだけのコンパイラを作成します。

整数リテラルを表示するだけのコンパイラはこんな感じです。整数リテラルノードが渡されたら、リテラルの値をレジスタ x0 へ格納し、プリント関数 p を呼び出して画面へ出力します。

# minrubyc.rb
require "minruby"

tree = minruby_parse(ARGF.read)

puts "\t.text"
puts "\t.align 2"
puts "\t.globl _main"
puts "_main:"
# lr レジスタと fp レジスタをスタックへ退避
puts "\tsub sp, sp, #16"
puts "\tstp fp, lr, [sp, #0]"

if tree[0] == "lit"
  # 整数リテラルの値を x0 レジスタへ格納
  puts "\tmov x0, ##{tree[1]}"
else
  raise "invalid AST: #{tree}"
end

# 終了する前に x0 レジスタの値を出力するため、p 関数を呼び出す
puts "\tbl _p"

# lr レジスタと fp レジスタをスタックから復元
puts "\tldp fp, lr, [sp, #0]"
puts "\tadd sp, sp, #16"

# 終了ステータスに 0 を返す
puts "\tmov x0, #0"
puts "\tret"

プリント関数 plibminruby.c へ定義しておきます。

// libminruby.c
#include <stdio.h>

long p(long n) {
    printf("%ld\n", n);
    return n;
}

早速コンパイルしてみましょう(コンパイル手順は後述)。4649 が出力されればOKです。

【ちょっと補足】コンパイル手順

MinRubyコンパイラのコンパイルは以下の流れで行われます。

# MinRubyのソースをコンパイルして foo.s へ出力
ruby minrubyc.rb foo.rb > foo.s

# foo.s と libminruby.c をコンパイル
gcc foo.s libminruby.c -o a.out

# 実行
./a.out

コンパイラへのMinRubyのソースの渡し方は、ファイルで渡しても良いし、標準入力から渡してもOKです。

四則演算の導入

整数リテラルの表示ができたので、次は四則演算を導入します。

以下が四則演算を導入したコンパイラのコードです。

右辺と左辺を評価した結果がそれぞれレジスタ x0 へ格納されるので、右辺と左辺の値を計算したのちレジスタ x0 へ格納します。

# minrubyc.rb
require "minruby"

def gen(tree)
  if tree[0] == "lit"
    puts "\tmov x0, ##{tree[1]}"
  elsif %w(+ - * /).include?(tree[0])
    # 四則演算
    op = tree[0]
    expr1 = tree[1]
    expr2 = tree[2]

    # 評価結果一時保持用のスタック領域を確保
    puts "\tsub sp, sp, #16"

    # x0 へ格納された左辺評価結果をスタックへ積む
    gen(expr1)
    puts "\tstr x0, [sp, #0]"

    # x0 へ格納された右辺評価結果をスタックへ積む
    gen(expr2)
    puts "\tstr x0, [sp, #8]"

    # スタックへ積んだ評価結果を x1 レジスタと x0 レジスタへロード
    puts "\tldr x1, [sp, #8]"
    puts "\tldr x0, [sp, #0]"

    # 演算結果を x0 へ格納
    case op
    when "+"
      puts "\tadd x0, x0, x1"
    when "-"
      puts "\tsub x0, x0, x1"
    when "*"
      puts "\tmul x0, x0, x1"
    when "/"
      puts "\tsdiv x0, x0, x1"
    else
      raise "invalid operator: #{op}"
    end

    # スタックを破棄
    puts "\tadd sp, sp, #16"
  else
    raise "invalid AST: #{tree}"
  end
end

tree = minruby_parse(ARGF.read)

puts "\t.text"
puts "\t.align 2"
puts "\t.globl _main"
puts "_main:"
# lr レジスタと fp レジスタをスタックに退避
puts "\tsub sp, sp, #16"
puts "\tstp fp, lr, [sp, #0]"

gen(tree)

# 終了する前に x0 レジスタの値を出力するため、p 関数を呼び出す
puts "\tbl _p"

# lr レジスタと fp レジスタをスタックから復元
puts "\tldp fp, lr, [sp, #0]"
puts "\tadd sp, sp, #16"

# 終了ステータスに 0 を返す
puts "\tmov w0, #10"
puts "\tret"

10 + 20 をコンパイルしてみます。30 が表示されればOKです。

【ちょっと補足】レジスタの使い方のルール

64ビットARMでは x0 から x30 までの31個の整数レジスタがあります(その他、浮動小数点レジスタなどもあります)。

レジスタには利用時のルールがいろいろあります。例えば関数の引数と戻り値に関しては以下のようなルールがあります。

  • 関数の引数は x0 から x7 へセットする
    • 1番目の引数は x0 へ、2番目の引数は x1 へ、みたいな感じで
    • (8個以上セットしたい場合のルールは割愛)
  • 関数の戻り値は x0 へセットする

MinCamlコンパイラではこれにならって、構文木の各ノードが値を返す時はレジスタ x0 へ値をセットするようになっています。また、組み込み関数やユーザー定義関数を呼び出す時の引数はレジスタ x0 から x7 へセットし、関数の戻り値は x0 へセットするようになっています。

参考資料

ARM64: ABI 規則

https://zenn.dev/hidenori3/articles/c9053a76be641c

プリント関数 p の導入

次は、整数をプリントする p 関数を導入します。

macOSではCで定義した関数名のプリフィックスに _ がつけられてしまうため、アセンブリから p 関数を呼び出す際は bl _p のようにアンダースコアをつけて呼び出します。

diff --git a/minrubyc.rb b/minrubyc.rb
index 780f97e..268a901 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -40,6 +40,11 @@ def gen(tree)
 
     # スタックを破棄
     puts "\tadd sp, sp, #16"
+  elsif tree[0] == "func_call" && tree[1] == "p"
+    # p 関数を呼び出す
+    expr = tree[2]
+    gen(expr)
+    puts "\tbl _p"
   else
     raise "invalid AST: #{tree}"
   end
@@ -57,9 +62,6 @@ puts "\tstp fp, lr, [sp, #0]"
 
 gen(tree)
 
-# 終了する前に x0 レジスタの値を出力するため、p 関数を呼び出す
-puts "\tbl _p"
-
 # lr レジスタと fp レジスタをスタックから復元
 puts "\tldp fp, lr, [sp, #0]"
 puts "\tadd sp, sp, #16"

p 5963 をコンパイルして 5963 が出力されればOKです。

複文(statements)の導入

次は以下のように複数のステートメントを評価できるようにします。

p 123
p 456
p 789

複文の対応は以下のとおり。stmts の中の複数のノードをぐるぐる回してコードを生成するだけでOKです。

diff --git a/minrubyc.rb b/minrubyc.rb
index 268a901..b08b705 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -45,6 +45,10 @@ def gen(tree)
     expr = tree[2]
     gen(expr)
     puts "\tbl _p"
+  elsif tree[0] == "stmts"
+    tree[1..].each do |stmt|
+      gen(stmt)
+    end
   else
     raise "invalid AST: #{tree}"
   end

コンパイル & 実行してみます。46495963 が出力されればOKです。

変数の導入

次は変数を導入します。

変数にセットした値はスタック上に格納されるため、定義される変数の数( var_assign の数)を数えて、その分だけスタック上に領域を確保します。

変数は宣言された順番でスタック上に並ぶので、ある変数が何番目に宣言されたかが分かれば、その変数がスタック上のどこのアドレスに格納されるのかが分かります。

var_assignが来たらスタック上の決められたアドレスへ値を格納し、var_ref が来たらスタック上の決められたアドレスから値を取得して返します。

diff --git a/minrubyc.rb b/minrubyc.rb
index b08b705..2bbdefa 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -1,7 +1,33 @@
 # minrubyc.rb
 require "minruby"
 
-def gen(tree)
+# tree 内に含まれる、var_assign で定義される変数名の一覧
+def var_names(tree)
+  if tree[0] == "var_assign"
+    [tree[1]]
+  elsif tree[0] == "stmts"
+    arr = []
+    tree[1..].each do |statement|
+      arr += var_names(statement)
+    end
+    arr
+  else
+    []
+  end
+end
+
+# スタックフレーム上の変数のアドレスをフレームポインタ(fp)からのオフセットとして返す
+# 例:
+#   ひとつ目の変数のアドレス = フレームポインタ(fp) + 16
+#   ふたつ目の変数のアドレス = フレームポインタ(fp) + 24
+#   ふたつ目の変数のアドレス = フレームポインタ(fp) + 32
+#   ...
+def var_offset(var, env)
+  # 変数1つにつき8バイトの領域が必要
+  env.index(var) * 8 + 16
+end
+
+def gen(tree, env)
   if tree[0] == "lit"
     puts "\tmov x0, ##{tree[1]}"
   elsif %w(+ - * /).include?(tree[0])
@@ -13,11 +39,11 @@ def gen(tree)
     puts "\tsub sp, sp, #16"
 
     # x0 へ格納された左辺評価結果をスタックへ積む
-    gen(expr1)
+    gen(expr1, env)
     puts "\tstr x0, [sp, #0]"
 
     # x0 へ格納された右辺評価結果をスタックへ積む
-    gen(expr2)
+    gen(expr2, env)
     puts "\tstr x0, [sp, #8]"
 
     # スタックへ積んだ評価結果を x1 レジスタと x0 レジスタへロード
@@ -43,32 +69,53 @@ def gen(tree)
   elsif tree[0] == "func_call" && tree[1] == "p"
     # p 関数を呼び出す
     expr = tree[2]
-    gen(expr)
+    gen(expr, env)
     puts "\tbl _p"
   elsif tree[0] == "stmts"
     tree[1..].each do |stmt|
-      gen(stmt)
+      gen(stmt, env)
     end
+  elsif tree[0] == "var_assign"
+    name, expr = tree[1], tree[2]
+
+    # 評価した値をスタック上のローカル変数領域へ格納
+    gen(expr, env)
+    puts "\tstr x0, [fp, ##{var_offset(name, env)}]"
+  elsif tree[0] == "var_ref"
+    name = tree[1]
+
+    # ローカル変数領域からx0へ値をロード
+    puts "\tldr x0, [fp, ##{var_offset(name, env)}]"
   else
     raise "invalid AST: #{tree}"
   end
 end
 
 tree = minruby_parse(ARGF.read)
+env = var_names(tree)
+lvar_size = env.size * 8
 
 puts "\t.text"
 puts "\t.align 2"
 puts "\t.globl _main"
 puts "_main:"
+
+# スタックフレームを確保
+# NOTE: スタックのサイズは16の倍数でなければならない
+puts "\tsub sp, sp, ##{16 + (lvar_size % 16 == 0 ? lvar_size : lvar_size + 8)}"
+
 # lr レジスタと fp レジスタをスタックに退避
-puts "\tsub sp, sp, #16"
 puts "\tstp fp, lr, [sp, #0]"
+puts "\tmov fp, sp"
 
-gen(tree)
+gen(tree, env)
 
 # lr レジスタと fp レジスタをスタックから復元
 puts "\tldp fp, lr, [sp, #0]"
-puts "\tadd sp, sp, #16"
+
+# スタックフレームを破棄
+# NOTE: スタックのサイズは16の倍数でなければならない
+puts "\tadd sp, sp, ##{16 + (lvar_size % 16 == 0 ? lvar_size : lvar_size + 8)}"
 
 # 終了ステータスに 0 を返す
 puts "\tmov w0, #10"

以下のコードを実行してみます。

a = 10
b = 20
c = 30
p a
p b
p c

いい感じに動いているようです。

比較演算子の導入

比較演算子( == != > < >= <=)を導入します。MinCamlコンパイラには bool 型がないため、比較演算の結果が真の時は整数の 1 を、偽の時は整数の 0 を返します。

diff --git a/minrubyc.rb b/minrubyc.rb
index 2bbdefa..a7bbb06 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -30,7 +30,7 @@ end
 def gen(tree, env)
   if tree[0] == "lit"
     puts "\tmov x0, ##{tree[1]}"
-  elsif %w(+ - * /).include?(tree[0])
+  elsif %w(+ - * / == != < <= > >=).include?(tree[0])
     op = tree[0]
     expr1 = tree[1]
     expr2 = tree[2]
@@ -60,6 +60,24 @@ def gen(tree, env)
       puts "\tmul x0, x0, x1"
     when "/"
       puts "\tsdiv x0, x0, x1"
+    when "=="
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, eq"
+    when "!="
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, ne"
+    when "<"
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, lt"
+    when "<="
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, le"
+    when ">"
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, gt"
+    when ">="
+      puts "\tcmp x0, x1"
+      puts "\tcset x0, ge"
     else
       raise "invalid operator: #{op}"
     end

いい感じに動いているようです。

条件分岐の導入

次は条件分岐を導入します。いわゆる if 文です。if文の構文木はこんな感じ。条件式が真の場合はTHEN句が評価され、偽の場合はELSE句が評価されます。

irb(main):003:0> minruby_parse "if 0 == 1; p 123; else p 345; end"
=>
["if",
 ["==", ["lit", 0], ["lit", 1]],
 ["func_call", "p", ["lit", 123]], # THEN句
 ["func_call", "p", ["lit", 345]]] # ELSE句

以下はコンパイラのコードです。

条件式を評価し、真の場合はTHEN句へ評価し、偽の場合はELSE句を評価します。

分岐先のラベル名をプログラム中で一意にするため、ラベル名に tree.object_id を付与しています。

diff --git a/minrubyc.rb b/minrubyc.rb
index a7bbb06..fba72e9 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -11,6 +11,13 @@ def var_names(tree)
       arr += var_names(statement)
     end
     arr
+  elsif tree[0] == "if"
+    # if文の中の変数も参照できるよう、ifの中のブロックにも var_assign を探しに行く
+    arr = []
+    arr += var_names(tree[2])
+    if tree[3]
+      arr += var_names(tree[3])
+    end
+    arr
   else
     []
   end
@@ -104,6 +111,24 @@ def gen(tree, env)
 
     # ローカル変数領域からx0へ値をロード
     puts "\tldr x0, [fp, ##{var_offset(name, env)}]"
+  elsif tree[0] == "if"
+    cond, texpr, fexpr = tree[1], tree[2], tree[3]
+    # 条件式を評価
+    puts "\t// 条件式を評価"
+    gen(cond, env)
+    puts "\tcmp x0, #0"
+
+    puts "\tbeq .Lelse#{tree.object_id}"
+
+    # 真の場合はtexprを評価
+    puts "\t// 真の場合"
+    gen(texpr, env)
+    puts "\tb .Lendif#{tree.object_id}"
+    puts ".Lelse#{tree.object_id}:"
+    # 偽の場合はfexprを評価
+    puts "\t// 偽の場合"
+    gen(fexpr, env) if fexpr
+    puts ".Lendif#{tree.object_id}:"
   else
     raise "invalid AST: #{tree}"
   end
@@ -126,6 +151,12 @@ puts "\tsub sp, sp, ##{16 + (lvar_size % 16 == 0 ? lvar_size : lvar_size + 8)}"
 puts "\tstp fp, lr, [sp, #0]"
 puts "\tmov fp, sp"
 
+# ローカル変数を0で初期化
+env.each do |var|
+  puts "\tmov x0, #0"
+  puts "\tstr x0, [fp, ##{var_offset(var, env)}]"
+end
+
 gen(tree, env)
 
 # lr レジスタと fp レジスタをスタックから復元

コンパイルしてみます。 123 が出力されればOKです。

このまま勢いで while 文も導入しちゃいます。

diff --git a/minrubyc.rb b/minrubyc.rb
index fba72e9..3840382 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -18,6 +18,9 @@ def var_names(tree)
       arr += var_names(tree[3])
     end
     arr
+  elsif tree[0] == "while"
+    puts "\t// while: #{tree}"
+    var_names(tree[2])
   else
     []
   end
@@ -129,6 +132,15 @@ def gen(tree, env)
     puts "\t// 偽の場合"
     gen(fexpr, env) if fexpr
     puts ".Lendif#{tree.object_id}:"
+  elsif tree[0] == "while"
+    cond, body = tree[1], tree[2]
+    puts ".Lwhile#{tree.object_id}:"
+    gen(cond, env)
+    puts "\tcmp x0, #0"
+    puts "\tbeq .Lendwhile#{tree.object_id}"
+    gen(body, env)
+    puts "\tb .Lwhile#{tree.object_id}"
+    puts ".Lendwhile#{tree.object_id}:"
   else
     raise "invalid AST: #{tree}"
   end

これもいい感じに動いているようです。

組み込み関数の呼び出し

次はlibminruby.c で定義した組み込み関数を呼び出せるようにします。 func_call が返す構文木はこんな感じ。

irb(main):001:0> minruby_parse "p my_add(10, 20)"
=> ["func_call",
      "p",
      ["func_call",
        "my_add",
        ["lit", 10],
        ["lit", 20]]]

libminruby.c へ関数 my_addを追加し、

// libminruby.c
#include <stdio.h>

long p(long n) {
    printf("%ld\n", n);
    return n;
}

// 組み込み関数テスト用
long my_add(long a, long b) {
    return a + b;
}

コンパイラを以下のように修正します。

上述した通り、関数を呼び出す時の引数はレジスタ x0 から x7 にセットします。 func_call の引数に渡ってきたノードを評価して x0 から x7 へ順番に渡します。通常8つを超える引数を取り扱う場合はスタックを通じた引数の受け渡しを行うのですが、今回は8つを超える引数はサポートせずにエラーを返すようにしました。

diff --git a/minrubyc.rb b/minrubyc.rb
index 3840382..94f1769 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -1,6 +1,9 @@
 # minrubyc.rb
 require "minruby"
 
+# 引数用レジスタの一覧
+PARAM_REGISTERS = %w(x0 x1 x2 x3 x4 x5 x6 x7)
+
 # tree 内に含まれる、var_assign で定義される変数名の一覧
 def var_names(tree)
   if tree[0] == "var_assign"
@@ -94,11 +97,27 @@ def gen(tree, env)
 
     # スタックを破棄
     puts "\tadd sp, sp, #16"
-  elsif tree[0] == "func_call" && tree[1] == "p"
-    # p 関数を呼び出す
-    expr = tree[2]
-    gen(expr, env)
-    puts "\tbl _p"
+  elsif tree[0] == "func_call"
+    name, *args = tree[1..]
+
+    # 引数用のレジスタは8つしかないので、引数が8個以上の場合はエラー
+    raise "too many arguments (given #{args.size}, expected 8)" if args.size > 8
+
+    # 引数を評価してスタックへ積む
+    args.reverse.each do |arg|
+      gen(arg, env)
+      puts "\tsub sp, sp, #16"
+      puts "\tstr x0, [sp, #0]"
+    end
+
+    # スタックへ詰んだ引数の値を、引数用レジスタへセット
+    args.each_with_index do |arg, i|
+      puts "\tldr #{PARAM_REGISTERS[i]}, [sp, #0]"
+      puts "\tadd sp, sp, #16"
+    end
+
+    # 関数呼び出し
+    puts "\tbl _#{name}"
   elsif tree[0] == "stmts"
     tree[1..].each do |stmt|
       gen(stmt, env)

コンパイルして動かしてみます。いい感じに動いてそうです。

ユーザー定義関数の導入

最後に、ユーザー定義関数を導入します。以下のように、ユーザーが関数を定義し、それを呼び出せるようにします。

def hello()
  860 # ハロー
end

p hello() #=> 860

上記のコードをパーサにかけると以下のような構文木が返ります。

irb(main):002:0> minruby_parse "def hello() 860; end; p hello()"
=> ["stmts",
     ["func_def",
       "hello",
       [],
       ["lit", 860]],
          ["func_call", "p", ["func_call", "hello"]]]

コンパイラは、func_def で定義された部分をアセンブリコードとして出力し、 func_call でそれを呼び出します。上記の構文木をアセンブリコードとして出力すると以下のようになります。ユーザー定義関数の名前が他のライブラリ関数の名前と衝突しないよう、関数名の先頭に _minruby_ というプリフィックスをつけています。例えば関数 hello はアセンブリコード上では _minruby_hello というラベルが付けられます。

  .text
    .align 2

;; func_def で定義されたユーザー定義関数
    .globl _minruby_hello
_minruby_hello:
    sub sp, sp, #16
    stp fp, lr, [sp, #0]
    mov fp, sp
    mov x0, #860
    ldp fp, lr, [sp, #0]
    add sp, sp, #16
    ret

    .globl _main
_main:
    sub sp, sp, #16
    stp fp, lr, [sp, #0]
    mov fp, sp

  ;; ユーザー定義関数 hello を呼び出す
    bl _minruby_hello

    sub sp, sp, #16
    str x0, [sp, #0]
    ldr x0, [sp, #0]
    add sp, sp, #16
    bl _minruby_p
    ldp fp, lr, [sp, #0]
    add sp, sp, #16
    mov w0, #10
    ret

以下、ユーザー定義関数を実装したコンパイラのコードです。主に以下のことを行なっています。

  • libminrby.c の関数たちに minruby_ のプリフィックスをつけた
  • 構文木から関数定義 func_defs を抽出する関数 func_defs を追加
  • 抽出した関数定義をアセンブリコードとして出力
diff --git a/libminruby.c b/libminruby.c
index a581930..2ff715c 100644
--- a/libminruby.c
+++ b/libminruby.c
@@ -1,12 +1,12 @@
 // libminruby.c
 #include <stdio.h>
 
-long p(long n) {
+long minruby_p(long n) {
     printf("%ld\n", n);
     return n;
 }
 
 // 組み込み関数テスト用
-long my_add(long a, long b) {
+long minruby_my_add(long a, long b) {
     return a + b;
 }
diff --git a/minrubyc.rb b/minrubyc.rb
index 94f1769..6c309d8 100644
--- a/minrubyc.rb
+++ b/minrubyc.rb
@@ -40,6 +40,25 @@ def var_offset(var, env)
   env.index(var) * 8 + 16
 end
 
+# ユーザー定義関数を構文木より抽出
+def func_defs(tree)
+  if tree[0] == "func_def"
+    {
+      # 関数名をキーにして [関数名, 引数, 関数本体] を格納
+      tree[1] => tree[1..]
+    }
+  elsif tree[0] == "stmts"
+    tmp_hash = {}
+    tree[1..].each do |stmt|
+      tmp_hash.merge!(func_defs(stmt))
+    end
+    tmp_hash
+  else
+    {}
+  end
+end
+
+# 構文木をアセンブリコードとして出力
 def gen(tree, env)
   if tree[0] == "lit"
     puts "\tmov x0, ##{tree[1]}"
@@ -97,6 +116,8 @@ def gen(tree, env)
 
     # スタックを破棄
     puts "\tadd sp, sp, #16"
+  elsif tree[0] == "func_def"
+    # 関数の定義はコンパイル時にコードとして出力されるため、実行時には何も行わなくて良い
   elsif tree[0] == "func_call"
     name, *args = tree[1..]
 
@@ -117,7 +138,7 @@ def gen(tree, env)
     end
 
     # 関数呼び出し
-    puts "\tbl _#{name}"
+    puts "\tbl _minruby_#{name}"
   elsif tree[0] == "stmts"
     tree[1..].each do |stmt|
       gen(stmt, env)
@@ -165,12 +186,55 @@ def gen(tree, env)
   end
 end
 
+# 関数定義をアセンブリコードとして出力
+def gen_func_def(func_def)
+  name, params, body = func_def
+  lenv = var_names(body)
+  env = params + lenv
+
+  # 名前が衝突しないように、関数名の先頭に _minruby_ を付与
+  puts "\t.globl _minruby_#{name}"
+  puts "_minruby_#{name}:"
+
+  # 関数プロローグ
+  lvar_size = env.size * 8
+  puts "\tsub sp, sp, ##{16 + (lvar_size % 16 == 0 ? lvar_size : lvar_size + 8)}" # NOTE: スタックのサイズは16の倍数でなければならない
+  puts "\tstp fp, lr, [sp, #0]"
+  puts "\tmov fp, sp"
+  # スタック上のパラメータ領域を初期化
+  params.each_with_index do |param, i|
+    puts "\tstr #{PARAM_REGISTERS[i]}, [fp, ##{var_offset(param, env)}]"
+  end
+  # ローカル変数を初期化
+  lenv.each do |var|
+    puts "\tmov x0, #0"
+    puts "\tstr x0, [fp, ##{var_offset(var, env)}]"
+  end
+
+  gen(body, env)
+
+  # 関数エピローグ
+  puts "\tldp fp, lr, [sp, #0]"
+  puts "\tadd sp, sp, ##{16 + (lvar_size % 16 == 0 ? lvar_size : lvar_size + 8)}" # NOTE: スタックのサイズは16の倍数でなければならない
+  puts "\tret"
+end
+
 tree = minruby_parse(ARGF.read)
 env = var_names(tree)
 lvar_size = env.size * 8
 
+# ユーザー定義関数を構文木より抽出
+func_defs = func_defs(tree)
+
 puts "\t.text"
 puts "\t.align 2"
+
+# ユーザー定義関数をアセンブリコードとして出力
+func_defs.values.each do |func_def|
+  gen_func_def(func_def)
+end
+
+# メイン関数
 puts "\t.globl _main"
 puts "_main:"

コンパイルして動かしてみます。 860 (ハロー)が表示されればOKです。

サンプルプログラムを動かしてみる

MinCaml コンパイラはこれで完成です。最後にサンプルプログラムとして10番目のフィボナッチ数を計算する以下のプログラムを動かしてみます。

# fib.rb

def fib(n)
  if n < 2
    n
  else
    fib(n - 1) + fib(n - 2)
  end
end

p fib(10)

コンパイルして実行してみます。10番目のフィボナッチ数である 55 が表示されればOKです。

終わりに

以上、簡易なコンパイラの作り方のご紹介でした。クリスマスの夜、グラス片手に「コンパーイ(Compile)」なんていかがですか?

また、この記事を読んで「もっと言語処理系について知りたい!」となった方は以下を読んでみると良いかも。

では、コンバイバイ〜。