PHPで論理型を文字列に変換する際の言語仕様をリファレンスから調べる & php-srcのソースを書き換えて挙動を変えてみる

最近 Laravel でWebアプリの開発を始めたのですが、PHP の言語仕様のうち論理型から文字列へのキャストが思っていた振る舞いと違っていたので仕様を調べてみました。

# 環境

M1 Mac 上に建てた Ubuntu のコンテナです。

```
$ uname -a
Linux b59ec1c5b53c 6.5.11-linuxkit #1 SMP PREEMPT Wed Dec  6 17:08:31 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
```

# まずはインタラクティブシェルで論理型を文字列に変換してみる

`False` は `0` にキャストされるものと思い込んでましたが結果は以下のようになります。

- `True` は `1` に変換される
- `False` は `空値` に変換される

```
$ /bin/php --version
PHP 8.1.2-1ubuntu2.14 (cli) (built: Aug 18 2023 11:41:11) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.2, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.2-1ubuntu2.14, Copyright (c), by Zend Technologies

$ /bin/php -a
Interactive shell

php > echo True;
1
php > echo False;
php > 
```

# リファレンスで仕様を確認してみる

## 型の相互変換

『型の相互変換』ページ内の『文字列のコンテクスト』の節を確認すると `echo` によって行われるのは文字列型への変換であることが分かります。

https://www.php.net/manual/ja/language.types.type-juggling.php

> これは、 echo, print や 文字列中の変数のパース、または 文字列演算子 を使った場合です。

しかしながら論理型から変換した場合の仕様については明示されておらず。

### 余談：遥か昔に同じこと聞いてる人がいた

同ページ下部の User Contributed Notes で同じ話題に触れてる人がいました。

https://www.php.net/manual/ja/language.types.type-juggling.php#24809

> Printing or echoing a FALSE boolean value or a NULL value results in an empty string:
> (string)TRUE //returns "1"
> (string)FALSE //returns ""
> echo TRUE; //prints "1"
> echo FALSE; //prints nothing!

2024年3月時点で「21 years ago」と表示されていたので2002年〜2003年頃の投稿でしょうか。
言語の歴史を感じます。

## 確認すべきは型キャスト

『型の相互変換』ページの『型キャスト』の節を確認すると『文字列への変換』というリンクがあります。
どうやら変換先の型のページにてまとめられている模様。

https://www.php.net/manual/ja/language.types.type-juggling.php#language.types.typecasting

> 型の間でキャストを行う際の動作は、必ずしも明確ではありません。 詳細については、以下の節を参照ください:
> 論理値への変換
> 整数への変換
> 浮動小数点数への変換
> 文字列への変換
> 配列への変換
> オブジェクトへの変換
> リソース型への変換
> NULL への変換
> 型の比較表

## 文字列への変換

遷移した先の文字列型のページ内には下記の記載があります。

https://www.php.net/manual/ja/language.types.string.php#language.types.string.casting

> bool の true は文字列の "1" に、 false は "" (空文字列) に変換されます。 これにより boolean と文字列の値を相互に変換することができます。

これにより真値は文字列の `1` に、偽値は `空文字列` に変換されるという言語仕様を一次情報から把握することができました。

### 余談：論理型を整数にキャストする場合

『型キャスト』の節にある『整数への変換』のリンクを飛んだ先を見てみると、真だと `1` 、偽だと `0` でした。

https://www.php.net/manual/ja/language.types.integer.php#language.types.integer.casting.from-boolean

> false は、0 (ゼロ) となり、 true は、1 となります。

文字列と数値で `False` のキャストの挙動が少し違うのは個人的に若干の違和感を感じます。

# `php-src` を書き換えてキャストの挙動を変えてみる

せっかくなので[公開されているPHPのソースコード](https://github.com/php/php-src)を書き換えて実装箇所を把握してみます。

と言ってもディレクトリ構造からして全く把握していないのでググりまくったりChatGPTに聞きまくったりした結果、型変換は下記のファイルで実装されていることが分かりました。

https://github.com/php/php-src/blob/master/Zend/zend_operators.c

## ソースの書き換え

リポジトリをクローンしてサクッとコードを書き換えます。
今回は分かりやすく以下のような挙動を目指します。

- `True` は文字列 `z` に変換
- `False` は文字列 `0` に変換

ソースの変更差分はこんな感じ。

```
$ git diff
diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c
index 8853d24549..8391aacd26 100644
--- a/Zend/zend_operators.c
+++ b/Zend/zend_operators.c
@@ -1016,9 +1016,9 @@ static zend_always_inline zend_string* __zval_get_string_func(zval *op, bool try
                case IS_UNDEF:
                case IS_NULL:
                case IS_FALSE:
-                       return ZSTR_EMPTY_ALLOC();
+                       return ZSTR_CHAR('0');
                case IS_TRUE:
-                       return ZSTR_CHAR('1');
+                       return ZSTR_CHAR('z');
                case IS_RESOURCE:
                        return zend_strpprintf(0, "Resource id #" ZEND_LONG_FMT, (zend_long)Z_RES_HANDLE_P(op));
                case IS_LONG:
```

## ビルド

READMEに記載の手順をベースにビルドしていきます。
なお今回はインタラクティブシェルで動作確認を行いたかったので以下を追加で対応してます。

- `libreadline-dev` のインストール
- `--with-readline` オプションの指定

あと動作確認ひとつのためにそんなに時間かけたくなかったので `make test` も端折りました。

```
$ sudo apt install -y pkg-config build-essential autoconf bison re2c \
                      libxml2-dev libsqlite3-dev
$ sudo apt install -y libreadline-dev
$ ./buildconf 
$ ./configure --enable-debug --with-readline
$ make -j`nproc`
$ sudo make install
```

## 動作確認

ビルドされたPHPランタイムは `/usr/local/bin/php` としてインストールされるのでこれを使って動作を見ていきます。

なおパッケージマネージャーでインストールしたPHPとはバージョンが異なってますが揃えるの面倒だったのでちょっと目を瞑ってください。

```
$ /usr/local/bin/php --version
PHP 8.4.0-dev (cli) (built: Mar 18 2024 02:28:50) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.4.0-dev, Copyright (c) Zend Technologies
```

さて、インタラクティブシェルでの型変換の結果は如何に。

```
$ /usr/local/bin/php -a
Interactive shell

php > echo True;
z
php > echo False;
0
php > 
```

変更したソースのとおりに真値が文字列 `z` 、偽値が文字列 `0` へキャストされていることが確認できました。

こんなランタイム残しておいてもいいことないのでさっさと削除してしまいましょう。

# まとめ

言語仕様を一次情報から調べる力はエンジニアとして大事な素養だと思っているので、ここにまとめた調べ方が誰かの役に立てば幸いです。