`BasicObject#?` は作れるか

## 概要
### 動機

* Rubyではメソッド名の末尾に `!` と `?` が使える
* `BasicObject#!` がある（→[マニュアル](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/=21.html)）
* → ならば `BasicObject#?` も作れるのでは？

※ ここには重大な誤解がある

### 結果

* 作りにくい
* *とても*使いにくい
* 悔しいのでRuby自体を改造してみた

## さっそく挑戦
`?` のつくメソッドは条件式に使うような真偽値（ `true / false` または `obj / nil`）を返す慣習がある。今回は最短のメソッド名に合わせて最もシンプルな真偽判定にしたいので、**オブジェクトをbooleanに変換したものを返す**ことにする[^simplest]。

[^simplest]: 本当に一番シンプルなのは [`Object#itself`](https://docs.ruby-lang.org/ja/latest/method/Object/i/itself.html) だが、それを再作成する意味は無いし `#?` という名前には合わないと判断した

### 実装
マニュアルにある `!` の再定義例と同じように、普通に `def` してみる。

```ruby:bo_test.rb
class BasicObject
  def ?
    !!self
  end
end
```

```text:$&#x20;ruby&#x20;bo_test.rb
bo_test.rb:2: warning: invalid character syntax; use ?\n
bo_test.rb:2: syntax error, unexpected '?'
        def ?
             ^
bo_test.rb:5: syntax error, unexpected keyword_end, expecting end-of-input
```

構文エラーになってしまった。何かがダメらしい。

---

仕方ないので [`Module#define_method`](https://docs.ruby-lang.org/ja/latest/method/Module/i/define_method.html) を使う。

```ruby:bo_test.rb
class BasicObject
  define_method(:?) do
    !!self
  end
end
```

```text:$&#x20;ruby&#x20;bo_test.rb
bo_test.rb:2: syntax error, unexpected tCHAR, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END
        define_method(:?) do
                         ^
bo_test.rb:5: syntax error, unexpected keyword_end, expecting end-of-input
```

違う構文エラーが出た。~~シンタックスハイライトがネタバレしているが~~、 `?` が特別な解釈をされてしまったらしい。

---

ならシンボルの文字列をクォートで囲む（あるいはシンボルをやめて文字列を与える）。

```ruby:bo_test.rb
class BasicObject
  define_method(:'?') do
    !!self
  end
end
```

```irb:$&#x20;irb
irb(main):001:0> require './bo_test'
=> true
irb(main):002:0> BasicObject.instance_methods.sort
=> [:!, :!=, :==, :"?", :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
```

きちんと登録された。inspectされたシンボルがダブルクォートで囲っていることからも、単純に `:?` ではいけないことが分かる。

### 利用
シンボルを使わないと定義できなかった時点で察するが、**普通のメソッド呼び出しは構文エラーになる**。 [`Object#send`](https://docs.ruby-lang.org/ja/latest/method/Object/i/send.html) などで呼び出す必要があり、使い勝手が悪い。（これをメソッドと呼んでいいのだろうか…？）

```irb
irb(main):003:0> nil.?   # ?の後ろに空白を入れてある
(irb):3: warning: invalid character syntax; use ?\s
SyntaxError: (irb):3: syntax error, unexpected '?', expecting '('
nil.?   # ...
     ^

irb(main):004:0> nil.send(:'?')
=> false
```

`#map` あたりならシンボルを渡すだけなのでまあまあ普通に使える。

```irb
irb(main):005:0> [true, false, nil, 0, "", []].map(&:'?')
=> [true, false, false, true, true, true]
```

`!` とは違い**単項演算子が用意されているわけではない**ので、式の前に置くことはできない。

```irb
irb(main):006:0> ?nil
SyntaxError: (irb):6: syntax error, unexpected '?'

irb(main):007:0> ?_   # irbで変数 _ は直前（5行目）の結果を参照する…のだが？
=> "_"
```

## 文法の確認
### `?` の使われ方
Rubyにおいて `?` はどんな意味を持つのか調べる。マニュアルの「[Rubyで使われる記号の意味](https://docs.ruby-lang.org/ja/latest/doc/symref.html)」を調べると以下が載っている。

* `?a` ： [**文字リテラル**](https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#char)[^literal]
* `def xx?` ： メソッド名の一部
* `xx ? yy : zz` ： [条件演算子](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#cond)（三項演算子）
* `/xxx?/` ： 正規表現の量指定子

これまでのよくわからないエラーの多くは `?` の次の文字を文字列に解釈しようとしていることを言っていた。 `?_` が `"_"` となったのも同じ。

[^literal]: 記号説明のページでは「文字列リテラル」としているが、リテラルのページでは「文字リテラル」を別に説明しているのでそちらに合わせた

### メソッド名の規則
マニュアルから探し出すのが難しいのだが、以下の記述を見つけた。

> 「[メソッド呼び出し(super・ブロック付き・yield)](https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html)」
> メソッド名には通常の識別子の他、識別子に ? または ! の続いたものが許されます。慣習として、述語(真偽値を返すメソッド)には ? を、同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッド(例: tr と tr!)には ! をつけるようになっています。

> 「[字句構造](https://docs.ruby-lang.org/ja/latest/doc/spec=2flexical.html)」
> Rubyの識別子は英文字またはアンダースコア('\_')から始まり、英文字、アンダースコア('\_')または数字からなります。識別子の長さに制限はありません。

> 「[クラス／メソッドの定義](https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#method)」
> メソッド名としては通常の識別子の他に、再定義可能な演算子(例: ==, +, - など [演算子式](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html) を参照)も指定できます([演算子式の定義](https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#operator)参照)。

ということは「メソッド名の末尾に `!` と `?` が使える」という認識は間違っていた（前に識別子が必要）。それでも `!` 単体がメソッド名になれているのは再定義可能な演算子だからであり、従って `?` 単体はメソッド名になれない。

とはいえ `#define_method` や `#send` などではできているので、ここで言うメソッド名は `def` の後など決められた場所に限定された話かもしれない。

## Rubyの改造
何となく悔しいので、 `?` をメソッド名にすることを妨げているRubyの文法自体をいじってしまう。

前節までを踏まえてより `!` に近づけるために以下のようにする。

* 単項演算子 `?` を導入
* `obj.?` でも呼び出し可能
* `def ?` で再定義可能
* メソッドを組み込みで実装

※ 見よう見まねでいじるので方法の正しさに自信は無い

### Rubyの用意
[README](https://github.com/ruby/ruby#how-to-compile-and-install)を参考に準備。確認のため一旦インストールまでしてみる。なお、rbenvを導入済みの環境で試したが、追加で autoconf と bison をインストールする必要があった（エラーメッセージで親切に教えてくれた）。

```bash
# ソースコードの用意
git clone https://github.com/ruby/ruby.git
cd ruby/
git checkout -b myruby v2_5_1

# Makefile作成
autoconf
./configure --prefix=${HOME}/usr

# コード修正後、
# コンパイルとインストール
make
make install

# Ruby実行
${HOME}/usr/bin/ruby -v
```

### メソッドを実装
`rb_obj_not` を真似て追加する。

```c:object.c
VALUE
rb_obj_test(VALUE obj)
{
	return RTEST(obj) ? Qtrue : Qfalse;
}

...

	rb_define_method(rb_cBasicObject, "?", rb_obj_test, 0);
```

この時点でコンパイルすれば、Rubyの範囲内で実装したときと同様に `nil.send(:'?')` などが使える。

最適化目的のコードもあるようなので追加。（これで足りているのかはよく分からない）

```c:internal.h
CONSTFUNC(VALUE rb_obj_test(VALUE obj));
```

```c:vm_insnhelper.c
static VALUE
vm_opt_test(CALL_INFO ci, CALL_CACHE cc, VALUE recv)
{
	if (vm_method_cfunc_is(ci, cc, recv, rb_obj_test)) {
		return RTEST(recv) ? Qtrue : Qfalse;
	}
	else {
		return Qundef;
	}
}
```

```c:insns.def
DEFINE_INSN
opt_test
(CALL_INFO ci, CALL_CACHE cc)
(VALUE recv)
(VALUE val)
{
	val = vm_opt_test(ci, cc, recv);

	if (val == Qundef) {
		PUSH(recv);
		CALL_SIMPLE_METHOD(recv);
	}
}
```

### 文法を追加
まず、トークン `'?'` は条件演算子が使用しているため別のトークン `tUQMARK` を定義する。

```ruby:defs/id.def
# VM ID         OP      Parser Token
token_ops = %[\
  ...
  Not           !
  UQmark        ?       UQMARK
  ...
]
```

```yacc:parse.y
...
%token tUMINUS          RUBY_TOKEN(UMINUS) "unary-"
%token tUQMARK          RUBY_TOKEN(UQMARK) "unary?"
...
```

[演算子の優先順位](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html)は `!` と同レベルのところに追加しておく。

```yacc:parse.y
...
%right '!' '~' tUPLUS tUQMARK
...
```

構文の規則は `'!'` を真似る。

```yacc:parse.y
expr            : command_call
                  ...
                | tUQMARK command_call
                    {
                        $$ = call_uni_op(method_cond($2, &@2), idUQmark, &@1, &@$);
                    }
                | '!' command_call
                  ...


op              : '|'           { ifndef_ripper($$ = '|'); }
                  ...
                | tUQMARK       { ifndef_ripper($$ = tUQMARK); }
                | '!'           { ifndef_ripper($$ = '!'); }
                  ...


arg             : lhs '=' arg_rhs
                  ...
                | tUQMARK arg
                    {
                        $$ = call_uni_op(method_cond($2, &@2), idUQmark, &@1, &@$);
                    }
                | '!' arg
                  ...
```

最後にスキャナに `tUQMARK` を判別させる。文字 `?` を読み取ったときの解析は `parse_qmark` で行われている。

```c:parse.y
static enum yytokentype
parse_qmark(struct parser_params *parser, int space_seen)
{
...
    c = nextc();
    if (IS_AFTER_OPERATOR()) {
        /* メソッド名を期待する状況 → ? はメソッド名と判断 */
        SET_LEX_STATE(EXPR_ARG);
        if (c != '@') pushback(c);
        return tUQMARK;
    }
    if (IS_BEG() || (IS_SPCARG(c) && arg_ambiguous('?'))) {
        /* 式の始まり → ? は単項演算子と判断（文字リテラルは潰す） */
        SET_LEX_STATE(EXPR_BEG);
        pushback(c);
        return tUQMARK;
    }
    if (c == -1) {
...
```

これで文字リテラルは実質動作しなくなってしまったので、付属のRubyスクリプトを直さないといけない。コンパイル中のエラーメッセージを頼りに直せば10ファイル程度で終わるが、それでは未修正のコードが残ってしまうので、RuboCopで一気に直す[^remain]。

```bash:文字リテラルを除去
gem install rubocop
echo 'AllCops:
  TargetRubyVersion: 2.5' > .rubocop.yml
git ls-files | xargs rubocop --auto-correct --only Style/CharacterLiteral

# 直さない方がよさそうなものを復元
git checkout -- sample/
```

以上で完成したと思ったのだが、実行して `?` を呼び出すと **NoMethodError** が出てしまった。

[^remain]: `?\C-a` のようなものは直されないが、テストコードの類に登場するだけなので影響は無いはず

### `Symbol` の動作を修正
`:"?"` と `:?` が別物と扱われてしまったので直す。原因は symbol.c で最初に「1文字の記号」と「演算子」を登録する際に `?` が二重登録されたせいのようなので、演算子のみにする。

```c:symbol.c
static void
Init_op_tbl(void)
{
	...

	for (i = '!'; i <= '~'; ++i) {
		if (!ISALNUM(i) && i != '_' && i != '?') { /* '?' を外すよう変更 */
			char c = (char)i;
			register_static_symid(i, &c, 1, enc);
		}
	}
	for (i = 0; i < op_tbl_count; ++i) {
		register_static_symid(op_tbl[i].token, op_tbl[i].name, op_tbl_len(i), enc);
	}
}
```

ついでに、 `:"?"` をinspectした際に `:?` となるよう~~やっつけで~~改善する。

```c:symbol.c
static int
rb_enc_symname_type(...)
{
    ...
      case '?':
        if (len == 1) return ID_JUNK;
      default:
    ...
}
```

### 実行

```irb:\$&#x20;\${HOME}/usr/bin/irb
irb(main):001:0> BasicObject.instance_methods.sort   # #? は組み込み済
=> [:!, :!=, :==, :?, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]

irb(main):002:0> ?true      # 単項演算子
=> true
irb(main):003:0> false.?    # メソッド呼び出し
=> false
irb(main):004:0> 0.send(:?) # シンボルにクォート不要 (※)
=> true
irb(main):005:0> ?x         # 文字リテラルとは認識されない
NameError (undefined local variable or method `x' for main:Object)

irb(main):006:0> class Hoge
irb(main):007:1>   def ?   # 普通に再定義できる
irb(main):008:2>     puts "#{self.class}##{__callee__} called"
irb(main):009:2>     super
irb(main):010:2>   end
irb(main):011:1> end
=> :?
irb(main):012:0> ?Hoge.new
Hoge#? called
=> true

irb(main):013:0> !?!?!?nil   # 何個並べても大丈夫
=> true
irb(main):014:0> ?:?.? ? ? ?:?:? ?:?.?   # 4番目の ? と3番目の : が条件演算子
=> true
```

(※) irbではうまく動作しない。Ctrl-Dでirbを終了させると結果が表示される。

### 課題

* parse.y の仕組みの把握
	* `LEX_STATE` や `arg_ambiguous` が分かっていない
* 文字リテラルとの共存
	* gemが意外と文字リテラルを使っていて動かない
	* `?\\` のようにスキャナで判別できる部分は簡単そう
	* `?(` などのとき後ろが式かどうか見分けられるか

### 雑多なメモ
parse.yの読み書きに取り組む中で初めて知ったこと。

* 構文エラーのメッセージにはトークンが出ている
	* `unexpected '?'` と言われたら、単なる文字 `?` という意味ではなく、条件演算子と認識されていることが確定する。
		* ただし、不正な文字リテラルの際にも条件演算子をあてるので、warningが無いか確認が必要。
	* 今回追加した単項演算子と認識されていれば `unexpected unary?` のように言われる。
* シンボルの怪現象： `(:!@) == (:!)` と `(:!@) != (:"!@")`
	* `:` で始まるシンボルをパースする際は、後ろの文字列はメソッド名や変数名を期待している。クォートで囲ったときだけは普通の文字列。
	* スキャン時にメソッド名 `!@` は特別に `!` と同じと扱われる。だから違うように見えるシンボルが同じになる。
		* この仕様は `?` でも真似した。 `def ?@` などできる。
	* 文字列の場合は書いた通り `!@` 。だからクォートの有無で異なるシンボルになる。
* オプション `-y` でスキャナやパーサの動作を確認できる
	* `ruby -yce '!nil' | sed 's/^Entering/\n&/'`

### 本来の目的
よくよく考えたら、文字リテラルとぶつかっているのは単項演算子であって、本来の目的であるメソッド作成とは関係ない。なら単項演算子を諦めればgemを問題なく使えるのではないか。

```bash:修正～gemインストール
vim parse.y   # parse_qmark から単項演算子の分を取り除く
git checkout v2_5_1 -- basictest/ bin/ ext/ lib/ sample/ spec/ test/ tool/ KNOWNBUGS.rb *prelude.rb
make && make install
${HOME}/usr/bin/gem install --no-document pry pry-doc
```

```pry:\$&#x20;\${HOME}/usr/bin/pry
[1] pry(main)> BasicObject.instance_methods.sort
=> [:!, :!=, :==, :?, :__binding__, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
[2] pry(main)> false.?
=> false
[3] pry(main)> 0.send(:?)
=> true

[4] pry(main)> ?true   # 単項演算子はもう無い
SyntaxError: unexpected '?', expecting end-of-input
?true
^
[4] pry(main)> ?x      # 文字リテラル復活
=> "x"

[5] pry(main)> class Hoge
[5] pry(main)*   def ?
[5] pry(main)*     puts "#{self.class}##{__callee__} called"
[5] pry(main)*     super
[5] pry(main)*   end
[5] pry(main)* end
=> :?
[6] pry(main)> Hoge.new.?
Hoge#? called
=> true
```

できた。

## 結論

* `BasicObject#?` はRubyのメソッド名の規則に反する
* `define_method(:'?')` や `send(:'?')` などを使っていいのなら作って使える
* Rubyを改造すれば当然いける
	* ただし単項演算子まで入れようとすると文字リテラルとぶつかる

## 参考

* [Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/doc/index.html)
* [Rubyソースコード完全解説](http://i.loveruby.net/ja/rhg/book/) 第2部「構文解析」