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

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

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)」なんていかがですか?

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

では、コンバイバイ〜。

RubyConf Taiwan 2023に弊社@junk0612が登壇します

2023年12月15日(金) から16日(土) の2日間にわたって台湾の台北で開催される RubyConf Taiwan 2023 に、永和システムマネジメントから @junk0612 が登壇します。

2023.rubyconf.tw

ここでは、登壇者の @junk0612 が講演内容について軽く紹介をします。

12月15日(金) 16:15-16:55 @junk0612 『Understanding Parser Generators surronding Ruby with Contributing Lrama』

Lrama は Bison の代替として CRuby 向けに作成された LALR パーサジェネレータです。RubyKaigi 2023 で yui-knk さんによって発表されました。

本発表では、Lrama にコントリビュートした内容 (Bison にある Named References という機能の実装) の話を中心に、下記についてお話しします。

  • BNF、文脈自由文法といったパーサ関連の基礎知識
  • Prism (YARP) 、Bison、Racc などの Ruby をとりまくパーサ関連プロダクト
  • Named References とはどういう機能なのか
  • Lrama の内部構造と、どのように実装したのか

この発表は大阪 Ruby 会議 03 で発表した内容のアップデート版になります。9月以降 Lrama にあったアップデートについても軽く触れるつもりですので、大阪で発表を聞いた方にも楽しんでいただけると思います。


また永和システムマネジメントからは登壇者の @junk0612 の他に @koic, @ima1zumi, @fugakkbn も現地参加し、Speaker Sponsor として協賛しています。

それでは本編をお楽しみに。台北でお会いしましょう。

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

agile.esm.co.jp

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

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

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

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

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

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

discord.gg

来月リリース予定の Ruby 3.3 や、直近で開催された Kaigi on Rails 2023 や RubyWorld Conference 2023 などに関する話題があるかもしれません。

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


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

agile.esm.co.jp

入力したコマンドをクリップボードへ自動コピーしたい

こんにちは。永和システムマネジメントの内角低め担当、畠山です。

今回は、ターミナルで入力したコマンドを自動的にクリップボードへコピーする zsh フックを書いたのでご紹介します。

これまでは「コマンドを入力→手作業でコマンドをコピー→作業ログへペースト」の3ステップで作業ログを記録していましたが、今回の zsh フックを使うことで「コマンドを入力→作業ログへペースト」の2ステップで作業ログを記録することができます。

~/.zshrc へ以下のスクリプトを追加すればOKです。

(注)私の環境は macOS なのでクリップボードへのコピーに pbcopy コマンドを使っています。macOS 以外の方は自分の環境に合わせたコマンドに置き換えてください。

# 入力したコマンドをクリップボードへコピーするフック関数
function preexec_copy_command() {
  if [ -n "$COPY_COMMAND" ]; then
    # 実行するコマンドをクリップボードへコピー
    echo -n $1 | pbcopy
  fi
}

# 自動コピー開始
function copy_start() {
  export COPY_COMMAND=1
}

# 自動コピー停止
function copy_stop() {
  export COPY_COMMAND=
}

# `add-zsh-hook` をロード
autoload -Uz add-zsh-hook

# preexec 時(コマンド実行直前)に preexec_copy_command を実行するようフックを追加
add-zsh-hook preexec preexec_copy_command

copy_start でコマンドの自動コピーを開始し、copy_stop で自動コピー停止します。

# 自動コピー開始
$ copy_start

# 自動コピー停止
$ copy_stop

使ってる様子はこんな感じ。入力する度にコマンドがクリップボードへ自動コピーされます。

うちのシェルはzshじゃないよーという方も、お使いのシェルに何かしらのフック機構があると思うので、そちらで試してみると良いかも。