記事の案内

「このブログには他にどういう記事があるの?」という方向けの案内です。 *1

がんばったもの

memo88.hatenablog.com memo88.hatenablog.com

続き: Rubyで素朴な自作言語のコンパイラを作った - memo88


memo88.hatenablog.com

ごっこ

いわゆる "Build your own 〜 from scratch" 的なもの、写経したものなど。

カテゴリ別

Ruby

Ruby カテゴリの記事一覧

LibreOffice

LibreOffice カテゴリの記事一覧

Emacs

Emacs カテゴリの記事一覧

Emacs 関連は一部内容が古くなっている気がします。

*1:一部 Qiita など他所に書いたものがあります

Reline.readmultiline ちょっと調べたメモ

Reline を使うと複数行編集ができるようなので、自分が使いそうな基本的な部分について調べてみました。

このリッチなのが標準で使えるの嬉しいですよね。ありがたや……。

RUBY_VERSION    #=> 3.0.0
Reline::VERSION #=> 0.2.5

最初の雛形

ブロックは必須。

require "reline"

PROMPT = "> "

loop do
  text =
    Reline.readmultiline(PROMPT) do |_|
      # このブロックについては後述
      true
    end

  # 編集が完了した複数行文字列を使った処理
  puts "text (#{text})"
end

これだけだとまだ複数行編集できないんですが、デバッグ用のログと履歴まわりを準備しておくと捗るので先にそっちを片付けます。

挙動確認のためのデバッグログ出力

デバッグ用の表示を同じターミナルに出力すると混ざって分かりにくいので、 ファイルに出力して別ターミナルで tail -F することに。

$log = File.open("debug.log", "a")

def debug(*args)
  $log.puts *args
  $log.flush
end

履歴の保存・復元

同じ入力を繰り返すのは面倒なので履歴まわりを用意しておきます。

Reline.readmultiline の第2引数 add_hist を true にすると Reline が履歴を覚えてくれて、カーソルキー上下や ctrl-n ctrl-p で履歴を辿れるようになります。

  text = Reline.readmultiline(PROMPT, true) do |input| ...

一度終了して次回実行したときに前回までの履歴が復元されてほしいので、シリアライズしてファイルに保存します。とりあえず JSON で適当に。

require "json"

HISTORY_FILE = "history"

def add_history(text)
  File.open(HISTORY_FILE, "a") { |f| f.puts JSON.generate(text) }
end

def load_history
  return unless File.exist?(HISTORY_FILE)

  File.read(HISTORY_FILE).each_line do |json|
    Reline::HISTORY << JSON.parse(json)
  end
end

Reline.readmultiline のブロック

  text =
    Reline.readmultiline(PROMPT, true) do |input|
      debug ""
      debug "-->> readmultiline block"
      debug input.inspect
      true
    end

こんな感じでデバッグ出力して動作を見てみると、 Enter キーが押されたタイミングでこのブロックが呼び出されているようだぞ、ということが確認できます。

11 Enter 22 Enter と入力したときのデバッグ出力:

-->> readmultiline block
"11\n"

-->> readmultiline block
"22\n"

このブロックは LineEditor#confirm_multiline_termination_proc にセットされ、

ed_newline
=> confirm_multiline_termination
=> @confirm_multiline_termination_proc

という流れで呼び出されるようです。 ed_newline という名前のメソッドから呼ばれているので、改行の入力がトリガーになっていると考えて良さそうな雰囲気です。

ブロックの呼び出しはこのようになっていて、

@confirm_multiline_termination_proc.(temp_buffer.join("\n") + "\n")

各行の末尾に LF の付いた文字列がブロックの引数に渡ってくることが分かります。


ブロックの評価値の扱いも見てみます。

class Reline::LineEditor
  # ...
  private def ed_newline(key)
    # ...
          if confirm_multiline_termination
            finish
          else
            key_newline(key)
          end

真だったら編集完了、偽だったら編集継続となるようです(見た感じでは)。


というわけで、

  1. 編集中の複数行文字列が引数としてブロックに渡ってくる
  2. 編集が完了しているかを判断し、完了している場合はブロックの評価値を真にする。途中だったら偽にする。

というあたりを踏まえて、ブロックの中身を修正します。 例として、末尾が ; になっていたら編集完了というルールにします。

  text =
    Reline.readmultiline(PROMPT, true) do |input|
      debug ""
      debug "--> readmultiline block"
      debug input.inspect

      finished = input.strip.end_with?(";")
      debug "finished (#{finished})"
      finished
    end

11 Enter ;22 Enter ; Enter と入力したときのデバッグ出力:

-->> readmultiline block
"11\n"
finished (false)       ... まだ編集の途中

-->> readmultiline block
"11\n;22\n"
finished (false)       ... まだ編集の途中

-->> readmultiline block
"11\n;22\n;\n"
finished (true)        ... 末尾が ; なので編集が完了したと判断

なるほど。基本的なことがやりたいだけであればこのくらい分かっていれば良さそうですね。

プロンプトをカスタマイズする

たとえば、最初の行とそれ以外で異なるプロンプトを表示したいといった場合、 Reline.prompt_proc に Proc オブジェクトをセットすることでカスタマイズできるようです。

PROMPT = "> "

Reline.prompt_proc =
  Proc.new do |lines|
    lines.each_with_index.map do |line, i|
      i == 0 ? PROMPT : "| "
    end
  end
$ ruby sample.rb 
> 11
| 22
| ;
text (11
22
;)
> 

この場合、Reline.prompt_proc で生成したプロンプト文字列が優先して使われ、Reline.readmultiline の第一引数で渡したプロンプト文字列は使われなくなるようです(表面的な挙動を見た感じでは)。

ただし、Reline.readmultiline の第一引数で渡したプロンプト文字列と長さが異なっていると履歴を移動した際にカーソル位置がずれるので、とりあえず同じ長さにしておくと良いようです。

まとめたもの

require "reline"
require "json"

HISTORY_FILE = "history"
PROMPT = "> "

$log = File.open("debug.log", "a")

def debug(*args)
  $log.puts *args
  $log.flush
end

def add_history(text)
  File.open(HISTORY_FILE, "a") { |f| f.puts JSON.generate(text) }
end

def load_history
  return unless File.exist?(HISTORY_FILE)

  File.read(HISTORY_FILE).each_line do |json|
    Reline::HISTORY << JSON.parse(json)
  end
end

def finished?(input)
  stripped = input.strip
  return true if stripped == "exit"
  return true if stripped.end_with?(";")

  false
end

Reline.prompt_proc =
  Proc.new do |lines|
    lines.each_with_index.map do |line, i|
      i == 0 ? PROMPT : "| "
    end
  end

load_history

loop do
  text =
    Reline.readmultiline(PROMPT, true) do |input|
      debug ""
      debug "-->> readmultiline block"
      debug input.inspect

      finished = finished?(input)
      debug "finished (#{finished})"
      finished
    end

  add_history text

  # 編集が完了した複数行文字列を使った処理
  puts "text (#{text})"

  break if text == "exit"
end

メモ

参考

関連

vm2gol v2 (57) 二項演算を左結合に変更



二項演算が右結合になっていたのを左結合に変えます。

例として 1 + 2 + 3 で見てみます。

変更前は

[:+,
  1,
  [:+, 2, 3]]

となるようにパースされていて、最終的に機械語になって実行されるときには

2 + 3
1 + {2 + 3 の結果}

という順番で実行されるようになっていました。

今回の変更により

[:+,
  [:+, 1, 2],
  3]

となるようにパースされ、

1 + 2
{1 + 2 の結果} + 3

という順番で実行されるようになります。


なぜ変更するかという話で言えば、 v3 を作っているときに右結合なのを忘れていて少しハマったのがきっかけです。 コードを書いている側としては、無意識のうちに他の一般的な言語と同じで左結合だろうと思っていたわけですね。自分で作ったのに。

すごく困るというわけではないですし、 「ライフゲームコンパイルできればよい」という観点で言えば正直どっちでも大差ありません。

どっちでもいいのですが、実装を大きく変えなければいけないわけでもないので、 だったらより良い(一般的で直感に反していない、自然な)形に変えておこう、という程度の動機です。

意図的に右結合にしていたということもなかったと思いますし、変えてしまっていいでしょう。


diff です。

--- a/vgparser.rb
+++ b/vgparser.rb
@@ -182,39 +182,39 @@ def parse_var
   end
 end
 
-def parse_expr_right(expr_l)
+def parse_expr_right
   t = peek()
 
-  if t.value == ";" || t.value == ")"
-    return expr_l
-  end
-
   case t.value
   when "+"
     consume "+"
     expr_r = parse_expr()
-    [:+, expr_l, expr_r]
+    [:+, expr_r]
 
   when "*"
     consume "*"
     expr_r = parse_expr()
-    [:*, expr_l, expr_r]
+    [:*, expr_r]
 
   when "=="
     consume "=="
     expr_r = parse_expr()
-    [:eq, expr_l, expr_r]
+    [:eq, expr_r]
 
   when "!="
     consume "!="
     expr_r = parse_expr()
-    [:neq, expr_l, expr_r]
+    [:neq, expr_r]
 
   else
     raise ParseError
   end
 end
 
+def next_binop?
+  %w[+ * == !=].include?(peek().value)
+end
+
 def parse_expr
   t_left = peek()
 
@@ -223,7 +223,12 @@ def parse_expr
     expr_l = parse_expr()
     consume ")"
 
-    return parse_expr_right(expr_l)
+    if next_binop?
+      op, expr_r = parse_expr_right()
+      return [op, expr_l, expr_r]
+    else
+      return expr_l
+    end
   end
 
   if t_left.type == :int || t_left.type == :ident
@@ -237,8 +242,12 @@ def parse_expr
         t_left.value
       end
 
-    parse_expr_right(expr_l)
-
+    if next_binop?
+      op, expr_r = parse_expr_right()
+      [op, expr_l, expr_r]
+    else
+      expr_l
+    end
   else
     raise ParseError
   end

重複している部分は次回リファクタリングします(diff が見づらくなるので)。

その他の修正

  • ラベルが見つからない場合はエラーにする (vgasm.rb)
    • 存在しない関数を呼び出すようなコードを書いた場合、 原因が分かりにくいエラーが VM での実行時に発生するようになっていて困ったので、 アセンブルの段階でエラーにしてしまうことに
  • VMコメントの整理
  • rename: codegen_〜 => gen_〜 (vgcg.rb)
    • メソッド名を短くしました。 gen_〜 でも分かるからいいかなと。


素朴な自作言語のコンパイラをRustに移植した


移植一覧に戻る


やっつけなので汚いです。ライフゲームコンパイルが通ったのでヨシ、というレベルの雑なものです。

github.com

移植元

memo88.hatenablog.com

ベースになっているバージョン: tag:56 のあたり

メモ

  • 理解は後回しにして、かっこ悪い書き方でもいいのでとにかく完成まで持って行く……といういつも通りの方針で、時間をかけすぎないように。理解は後でゆっくり。まずは手を動かして慣れる。
  • Tour of Rust
    • 情報量が多すぎずよく整理されていて、一番最初にこれを読めばよかった
  • 借用とかムーブとか
    • このくらいのプログラムを書いて動かせる程度には分かってきた、はず。 でもけっこう怪しい。
  • ライフタイム
    • まだよく分かっていなくて、コンパイルが通らなかったらライフタイムが絡まない書き方にして回避したりしている
  • 連結リストが難しそうだったので Vec<NodeId> で管理する方式にしてとりあえず回避
  • レキサの入力文字列は最初に Vec<char> にして使い回し
    • C版Zig版 のように単純なバイト列として扱ってもよかったが、 せっかくなので UTF-8 文字列として扱ってみた

TODO

気が向いたらあとで

  • List にノードID ではなくノードを直接持たせる
  • List のイテレータ対応