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
240
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
夢の無限スパゲッティ製造機 -実装篇- #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
PHPで使える日時の表現と、その知り方 #frontend_phpcon_do
o0h
PRO
0
210
Composerを使ったサプライチェーン攻撃の様子を眺めてみる #phpstudy
o0h
PRO
2
240
ソースコード→AST→オペコード、の旅を覗いてみる
o0h
PRO
1
170
PCOVから学ぶコードカバレッジ #phpcon_odawara
o0h
PRO
0
370
夢の無限スパゲッティ製造機 #phperkaigi
o0h
PRO
0
490
PHPer Book Revue 「雑に作る」 #phperkaigi
o0h
PRO
0
380
俺にも私がAIと作った オススメの個人ツールを語らせてくれ
o0h
PRO
0
74
#phperbiglt のLT
o0h
PRO
0
110
手軽に積ん読を増やすには?/読みたい本と付き合うには?
o0h
PRO
1
280
Other Decks in Programming
See All in Programming
Signal Forms: Beyond the Basics @ngBaguette 2026 in Paris
manfredsteyer
PRO
0
230
ECSアプリログをFireLensでコスト削減しようとしたけど諦めた話 in Fargate×Node.js
akihisaikeda
2
3.8k
Java × distroless で 軽量なコンテナイメージを / Java on Distroless
contour_gara
0
510
「AIで開発し、AIを届ける」をEvalでつなぐ 〜AIネイティブに始めるプロダクト開発の実践〜 / Connecting "Develop with AI, deliver AI" with Eval
rkaga
4
4.8k
正しくソフトウェアを作る、前提を疑うための認知の視点 / doubt-premise
minodriven
17
6.3k
Datadog × OpenTelemetry 入門と実践のあいだ
kn_to_maxpno
1
150
Lemonade + Foundry Toolkit でお手軽アプリ開発
seosoft
1
310
AIエージェントの隔離技術の徹底比較
kawayu
0
470
過去最大のMCPアップデート! 2026-07-28 RC版の謎に迫る
licux
3
140
ふつうのFeature Flag実践入門
irof
7
3.6k
3Dシーンの圧縮
fadis
1
680
jQueryをバージョンアップする前に使いたいjQuery Migrate
matsuo_atsushi
0
200
Featured
See All Featured
Building an army of robots
kneath
306
46k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
600
Abbi's Birthday
coloredviolet
2
8k
Fashionably flexible responsive web design (full day workshop)
malarkey
408
66k
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
250
The Curious Case for Waylosing
cassininazir
1
380
We Have a Design System, Now What?
morganepeng
55
8.2k
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
2.3k
Documentation Writing (for coders)
carmenintech
77
5.4k
The Director’s Chair: Orchestrating AI for Truly Effective Learning
tmiket
1
190
The Pragmatic Product Professional
lauravandoore
37
7.3k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
55
3.4k
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
おしまい! お付き合いいただき ありがとうございました!!
おしまい! お付き合いいただき ありがとうございました!!