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
hideki kinjyo
PRO
March 25, 2026
Programming
0
24
夢の無限スパゲッティ製造機 -実装篇- #phpstudy
PHP勉強会@東京 第185回の発表資料です。
前段の話:
https://speakerdeck.com/o0h/phperkaigi-2026
hideki kinjyo
PRO
March 25, 2026
Tweet
Share
More Decks by hideki kinjyo
See All by hideki kinjyo
夢の無限スパゲッティ製造機 #phperkaigi
o0h
PRO
0
330
PHPer Book Revue 「雑に作る」 #phperkaigi
o0h
PRO
0
250
俺にも私がAIと作った オススメの個人ツールを語らせてくれ
o0h
PRO
0
30
#phperbiglt のLT
o0h
PRO
0
74
手軽に積ん読を増やすには?/読みたい本と付き合うには?
o0h
PRO
1
240
symfony/mcp-bundleで、既存アプリケーションもお手軽にMCPサーバー化
o0h
PRO
1
120
組織もソフトウェアも難しく考えない、もっとシンプルな考え方で設計する #phpconfuk
o0h
PRO
10
5.7k
Composerが「依存解決」のためにどんな工夫をしているか #phpcon
o0h
PRO
1
690
Composerの依存解決 #phpstudy
o0h
PRO
0
180
Other Decks in Programming
See All in Programming
Claude Codeログ基盤の構築
giginet
PRO
7
3.5k
脱 雰囲気実装!AgentCoreを良い感じにWEBアプリケーションに組み込むために
takuyay0ne
3
380
Ruby and LLM Ecosystem 2nd
koic
1
1.2k
Codexに役割を持たせる 他のAIエージェントと組み合わせる実務Tips
o8n
4
1.4k
AI Assistants for Your Angular Solutions
manfredsteyer
PRO
0
150
コーディングルールの鮮度を保ちたい / keep-fresh-go-internal-conventions
handlename
0
230
CSC307 Lecture 15
javiergs
PRO
0
260
Rethinking API Platform Filters
vinceamstoutz
0
140
20260313 - Grafana & Friends Taipei #1 - Kubernetes v1.36 的開發雜記:那些困在 Alpha 加護病房太久的 Metrics
tico88612
0
230
Redox OS でのネームスペース管理と chroot の実現
isanethen
0
400
車輪の再発明をしよう!PHP で実装して学ぶ、Web サーバーの仕組みと HTTP の正体
h1r0
2
290
ポーリング処理廃止によるイベント駆動アーキテクチャへの移行
seitarof
3
1.1k
Featured
See All Featured
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
1
3.5k
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.6k
The Limits of Empathy - UXLibs8
cassininazir
1
270
The Impact of AI in SEO - AI Overviews June 2024 Edition
aleyda
5
770
Everyday Curiosity
cassininazir
0
170
Automating Front-end Workflow
addyosmani
1370
200k
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
How to train your dragon (web standard)
notwaldorf
97
6.6k
Faster Mobile Websites
deanohume
310
31k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
22k
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
2.5k
Speed Design
sergeychernyshev
33
1.6k
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
おしまい! お付き合いいただき ありがとうございました!!
おしまい! お付き合いいただき ありがとうございました!!