Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
夢の無限スパゲッティ製造機 -実装篇- #phpstudy
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
hideki kinjyo
PRO
March 25, 2026
Programming
220
0
Share
夢の無限スパゲッティ製造機 -実装篇- #phpstudy
PHP勉強会@東京 第185回の発表資料です。
前段の話:
https://speakerdeck.com/o0h/phperkaigi-2026
hideki kinjyo
PRO
March 25, 2026
More Decks by hideki kinjyo
See All by hideki kinjyo
ソースコード→AST→オペコード、の旅を覗いてみる
o0h
PRO
0
9
PCOVから学ぶコードカバレッジ #phpcon_odawara
o0h
PRO
0
280
夢の無限スパゲッティ製造機 #phperkaigi
o0h
PRO
0
450
PHPer Book Revue 「雑に作る」 #phperkaigi
o0h
PRO
0
340
俺にも私がAIと作った オススメの個人ツールを語らせてくれ
o0h
PRO
0
57
#phperbiglt のLT
o0h
PRO
0
88
手軽に積ん読を増やすには?/読みたい本と付き合うには?
o0h
PRO
1
260
symfony/mcp-bundleで、既存アプリケーションもお手軽にMCPサーバー化
o0h
PRO
1
150
組織もソフトウェアも難しく考えない、もっとシンプルな考え方で設計する #phpconfuk
o0h
PRO
10
5.8k
Other Decks in Programming
See All in Programming
ハーネスエンジニアリングにどう向き合うか 〜ルールファイルを超えて開発プロセスを設計する〜 / How to approach harness engineering
rkaga
24
15k
Server-Side Kotlin LT大会 vol.18 [Kotlin-lspの最新情報と Neovimのlsp設定例]
yasunori0418
1
200
How We Benchmarked Quarkus: Patterns and anti-patterns
hollycummins
1
160
NakouPAY説明用
annouim0
0
270
クラウドネイティブなエンジニアに向ける Raycastの魅力と実際の活用事例
nealle
2
220
10 Tips of AWS ~Gen AI on AWS~
licux
5
480
Kubernetes上でAgentを動かすための最新動向と押さえるべき概念まとめ
sotamaki0421
3
720
運転動画を検索可能にする〜Cosmos-Embed1とDatabricks Vector Searchで〜/cosmos-embed1-databricks-vector-search
studio_graph
1
490
YJITとZJITにはイカなる違いがあるのか?
nakiym
0
260
VueエンジニアがReactを触って感じた_設計の違い
koukimiura
0
190
エラー処理の温故知新 / history of error handling technic
ryotanakaya
7
1.7k
ルールルルルルRubyの中身の予備知識 ── RubyKaigiの前に予習しなイカ?
ydah
1
220
Featured
See All Featured
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
120
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
38
2.8k
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
130
Game over? The fight for quality and originality in the time of robots
wayneb77
1
160
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
We Have a Design System, Now What?
morganepeng
55
8.1k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
1
530
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
900
Building a Scalable Design System with Sketch
lauravandoore
463
34k
Discover your Explorer Soul
emna__ayadi
2
1.1k
Building Better People: How to give real-time feedback that sticks.
wjessup
370
20k
Transcript
夢の無限スパゲッティ製造機 -実装篇- 第185回 PHP勉強会@東京 Hideki Kinjyo GitHub: o0h / X:
@o0h_ [発表用] v1.0.0
夢の無限スパゲッティ製造機 -実装篇- #phpstudy.185 Hideki Kinjyo GitHub:o0h / X:@o0h_
自己紹介 • 金城秀樹 / きんじょうひでき • GitHub: @o0h / 𝕏
: @o0h_ • 好きなスパゲッティはカルボナーラ • アイコンは美味しい鮭親子丼のChef ver.です • 何故ならスパゲッティを作っていますからね • 最近はPodcastをやっています • ハッシュタグ: #readlinefm 3
3月22日に発表したやつ 4
今日のお話 PHPerKaigiでは、生成されるコードの話がメインだったので その実装についての話をします 1. そもそも何これ 2. 全体の処理の流れ 3. 前半戦: 元PHPコードから「中間表現」まで
4. 後半戦: 「中間表現」から PHPコードへ 5
1. そもそも何これ 2. 全体の処理の流れ 3. 前半戦: 元PHPコードから「中間表現」まで 4. 後半戦: 「中間表現」から
PHPコードへ
The Spaghetti Dream #とは • PHPで書かれたコードを「gotoベースのコード」に変換するプログラム • 制御構文の排除 • クラスやそれに類する機能の排除
• もともとは、「構造化プログラミング」の説明を読んで 「何を言っているんだ、当たり前じゃない…?」と思ったのが始まり • それが無い世界を味わってみたくなった • PHPerKaigi 2025 『PHPによる"非"構造化プログラミング入門』 • 「gotoを使わなくても制御フローを実現できる」なら、 「今のコードをgotoでも作れる」のではないか 7
DEMO the-spaghetti-dream.nichiyou.be
1. PHPで書かれたソースコードをインプットとして 2. いったんオペコードに変換し 3. 再びPHPの表現に戻す! <?php $tmp0 = …
sub: ————— goto hoge; end: 何をしているのか 9 ソースコード オペコード <?php function hoge($x) { ————— } 0 INIT_FCALL 'hoge' 1 SEND_VAL 1 2 SEND_VAL 10 3 DO_ICALL $0 4 ECHO $0 5 RETURN 1 オペコード スパゲッティ
オペコードってこんなもの • 通常のPHPのコードを、ZendVM(仮想マシン)が使える形に変換したもの • 制御フローは、「CPU(機械)向けに近く、単純化」された形に • ifやforが無くなり、「ジャンプ命令」に書き換えらえる 10
オペコードってこんなもの • 基本的に上から実行される • 1行 = 1ステップ 11
オペコードってこんなもの • 各行は、インデックス・命令・引数の3つのパーツで構成される • 引数の数は命令によって異なる 12 命令(オペコード) 引数(オペランド) インデックス(番号)
オペコードってこんなもの • 「CV0にtrueを代入する」 13 boolのtrue ローカル変数 (CV0 = $iikanji) ASSIGN:
引数1に引数2を代入する
オペコードってこんなもの • 「CV0をチェックして、値が `0` だったら、3行目にジャンプする」 14 JMPZ: 引数1がゼロだったら、 引数2の番号にジャンプする ローカル変数
(CV0 = $iikanji) 番号3
こんな感じのやつをアレコレする話です • これを変換していきましょう!! • 変換できると嬉しいですよね! 15 <?php $tmp0 = …
sub: ————— goto hoge; end: ソースコード オペコード <?php function hoge($x) { ————— } 0 INIT_FCALL 'hoge' 1 SEND_VAL 1 2 SEND_VAL 10 3 DO_ICALL $0 4 ECHO $0 5 RETURN 1 オペコード スパゲッティ
1. そもそも何これ 2. 全体の処理の流れ 3. 前半戦: 元PHPコードから「中間表現」まで 4. 後半戦: 「中間表現」から
PHPコードへ
全体フロー 17 コマンド起動/src受け取り オペコード変換 オペコードに無い情報の補完 (依存ファイル読み込み) 🍝化 冗長なコードのクリーンアップ
None
1. そもそも何これ 2. 全体の処理の流れ 3. 前半戦: 元PHPコードから「中間表現」まで 4. 後半戦: 「中間表現」から
PHPコードへ
やっていること 1. インプットとなるソースコードを受け取る 2. オペコードの出力の実行 3. オペコードのフレームごとに `OpCodeCollection`を作成 • `\O0h\SpaghettiDream\Extractor\IR\OpcodeCollection`
4. 各ステップを`OpCode` クラスのオブジェクトに変換 • `\O0h\SpaghettiDream\Extractor\IR\Opcode` 20
オペコードの出力 • オペコードの出力には色々な方法がある • PHPをそのまま使う: php -d opcache.opt_debug_level=0x10000 • 拡張を入れる:
VLD • コマンドを使う: phpdbgコマンド • 今回はphpdbgコマンドを使っている • スクリプトの実行をせずにオペコードだけ取り出したかったので 21
オペコードの出力 • 素朴にexec() 22 $command = sprintf( '%s -n -p\\*
%s 2>&1', escapeshellcmd($this->phpdbgPath), escapeshellarg($phpFilePath), ); $output = []; $returnCode = 0; exec($command, $output, $returnCode);
オペコードのフレームごとに `OpCodeCollection`を作成 • オペコードに変換された時に、スコープごとにフレームが形成される 23 <?php function add($a, $b) {
return $a + $b; } function sub($a, $b) { return $a - $b; } var_dump( sub(100,add(10, 20)) ); add: ; (lines=7, args=2, vars=2, tmps=2) ; /private/tmp/piyo.php:3-6 L0003 0000 CV0($a) = RECV 1 L0003 0001 CV1($b) = RECV 2 ɾ ɾ sub: ; (lines=7, args=2, vars=2, t ; /private/tmp/piyo.php:8-11 L0008 0000 CV0($a) = RECV 1 L0008 0001 CV1($b) = RECV 2 ɾ ɾ $_main: ; (lines=13, args=0, ; /private/tmp/piyo. L0013 0000 EXT_STMT L0013 0001 INIT_FCALL 1 1 ɾ ɾ
Extractor\IR\OpcodeCollection • 1フレームに対して1インスタンス 24 sub: ; (lines=7, args=2, vars=2, tmps=2)
; /private/tmp/piyo.php:8-11 L0008 0000 CV0($a) = RECV 1 L0008 0001 CV1($b) = RECV 2 ɾ ɾ add: ; (lines=7, args=2, vars=2, tmps=2) ; /private/tmp/piyo.php:3-6 L0003 0000 CV0($a) = RECV 1 L0003 0001 CV1($b) = RECV 2 ɾ ɾ $_main: ; (lines=13, args=0, vars=0, tmps=4) ; /private/tmp/piyo.php:1-17 L0013 0000 EXT_STMT L0013 0001 INIT_FCALL 1 112 string("var_dump") ɾ ɾ OpcodeCollection OpcodeCollection OpcodeCollection
オペコードのパース • 1行ごとに地道に正規表現 でパースしていく • とはいえ、普通のPHPより はよっぽどシンプル 25
Extractor\IR\Opcode • オペコードの1行に対して1インスタンス • ここでは素直にパースして、難しいことは後で考える 26 new Opcode( line: 3,
offset: 0, opcode: 'ASSIGN', operands: [ new Operand(TYPE_CV, 0, 'a'), new Operand(TYPE_CONST, 1), ], ) L0003 0000 ASSIGN CV0($a) int(1)
Extractor\IR\Opcode • オペコードの1行に対して1インスタンス • ここでは素直にパースして、難しいことは後で考える 27 new Opcode( line: 5,
offset: 3, opcode: 'JMPZ', operands: [ new Operand(TYPE_TMP, 2), new Operand(TYPE_JUMP, 7), ], ) L0005 0003 JMPZ T2 ->7
Extractor\IR\Operand • オペランド1つに対して1インスタンス • ここも素直にパースして、難しいことは後で考える 28 new Operand(TYPE_TMP, 2) L0005
0003 JMPZ T2 ->7 new Operand(TYPE_JUMP, 7)
1. そもそも何これ 2. 全体の処理の流れ 3. 前半戦: 元PHPコードから「中間表現」まで 4. 後半戦: 「中間表現」から
PHPコードへ
• OpcodeCollectionをイテレーションして(-> OpCodeを取り出す)、 OpcodeTranslatorを介してPHPコードに変換していく 後半戦でやること 30 foreach ($opcodes as $opcode)
{ $label = $this->labelManager ->formatLabelDefinition($opcode->offset, $scopePrefix); try { $code = $this->translator->translate($opcode, $context); } catch (GeneratorException $e) { throw new GeneratorException(
• オペコードごとに対応する変換器を判定して、変換を実行 Generator\OpcodeTranslator 31 public function translate(Opcode $opcode, Context $context):
?string { foreach ($this->translators as $translator) { if ($translator->supports($opcode)) { return $translator->translate($opcode, $context); } } throw new GeneratorException( "Unsupported opcode: {$opcode->opcode}"); }
• Translatorはたくさんある • オペコード:Translator は、1:1関係ではない 色々なTranslator 32
例: ComparisonTranslator 33 <?php $a = time(); $b = time();
var_dump($a < $b);
例: ComparisonTranslator 34 class ComparisonTranslator implements TranslatorInterface { public function
supports(Opcode $opcode): bool { return isset(self::OPCODE_OPERATORS[$opcode->opcode]); }
例: ComparisonTranslator 35 class ComparisonTranslator implements TranslatorInterface { private const
OPCODE_OPERATORS = [ 'IS_EQUAL' => '==', 'IS_NOT_EQUAL' => '!=', 'IS_IDENTICAL' => '===', 'IS_NOT_IDENTICAL' => '!==', 'IS_SMALLER' => '<', 'IS_SMALLER_OR_EQUAL' => '<=', 'SPACESHIP' => '<=>', 'CASE_STRICT' => '===', ];
例: ComparisonTranslator 36 class ComparisonTranslator implements TranslatorInterface { public function
translate(Opcode $opcode, Context $context): ?string { $operator = self::OPCODE_OPERATORS[$opcode->opcode]; $operands = $opcode->operands; $left = $context->getValue($operands[0]); $right = $context->getValue($operands[1]); $resultVar = $context->extractResultVar($opcode); return "{$resultVar} = {$left} {$operator} {$right};"; } }
before / after 37 <?php $a = time(); $b =
time(); var_dump($a < $b); <?php // ===== Main code $_tmp2 = time(); $a = $_tmp2; $_tmp4 = time(); $b = $_tmp4; $_tmp6 = $a < $b; var_dump($_tmp6); goto __end; __end:
オペコードから取れないデータの補完
通常のオペコードには現れないものがあるんですよね • 値が決定されている配列とか • 関数の引数とか • クラス定数とか • etc 39
定数配列 • 次のコードのオペコードを生成すると、 • 通常は、このように配列の中身が現れない • 夢の無限スパゲッティ製造機だと、こうなる $config1 = ['name'
=> 'test', 'value' => 42]; L0003 0000 ASSIGN CV0($config) array(...) 3: ASSIGN CV0($config1) "[\'name\' => \'test\',\'value\' => 42]" 40
valueに 定数を使った配列
🍝
値がない配列を見つけたら
元のコードの行番号を取って
token_get_allで解析
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], token_get_allの結果 (説明用に整形しています)
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], 0:トークンの識別ID 1: ソース 2: 行番号
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], array(…)があった 「2行目」のトークンを探して
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], array の開始位置を見つけたら
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], そこから「配列の中身」を抜き出す = "[" と "]" の間のトークン
"=", [397, " ", 2], "[", [269, "'name'", 2], [397,
" ", 2], [391, "=>", 2], [397, " ", 2], [269, "'test'", 2], ",", [397, " ", 2], [269, "'value'", 2], [397, " ", 2], [391, "=>", 2], [397, " ", 2], ['name' => 'test', 'value' => 42]
ちなみに: valueに変数を使った配列の場合 52
参考資料 • PHP7から定数配列がOPcacheに乗るので巨大配列が使い放題という話 - hnwの日記 https://hnw.hatenablog.com/entry/2020/08/12/212433 53
そんな感じでできあがり(まだ始まったばかりだ)
よかったら遊んでみてくださいね • https://the-spaghetti-dream.nichiyou.be/ • さくらのAppRunさんを使っています!! • 簡単!最高!! 55
おまけ • なんやかんやで、 Slimを使ったアプリケーションの スパゲッティ化に成功しました • →のコードが変換元(32行) => 53,957行 •
github.com/o0h/the-spaghetti-dream-gallery を見てみてください • 僕は怖くて中身を見ていません • ただし変換前後のコードに対して 同じ内容のE2Eテストはパス済み(thx runn!) 56
おしまい! お付き合いいただき ありがとうございました!!
おしまい! お付き合いいただき ありがとうございました!!