Upgrade to Pro — share decks privately, control downloads, hide ads and more …

夢の無限スパゲッティ製造機 -実装篇- #phpstudy

夢の無限スパゲッティ製造機 -実装篇- #phpstudy

PHP勉強会@東京 第185回の発表資料です。
前段の話: https://speakerdeck.com/o0h/phperkaigi-2026

Avatar for hideki kinjyo

hideki kinjyo PRO

March 25, 2026
Tweet

More Decks by hideki kinjyo

Other Decks in Programming

Transcript

  1. 自己紹介 • 金城秀樹 / きんじょうひでき • GitHub: @o0h / 𝕏

    : @o0h_ • 好きなスパゲッティはカルボナーラ • アイコンは美味しい鮭親子丼のChef ver.です • 何故ならスパゲッティを作っていますからね • 最近はPodcastをやっています • ハッシュタグ: #readlinefm 3
  2. The Spaghetti Dream #とは • PHPで書かれたコードを「gotoベースのコード」に変換するプログラム • 制御構文の排除 • クラスやそれに類する機能の排除

    • もともとは、「構造化プログラミング」の説明を読んで 「何を言っているんだ、当たり前じゃない…?」と思ったのが始まり • それが無い世界を味わってみたくなった • PHPerKaigi 2025 『PHPによる"非"構造化プログラミング入門』 • 「gotoを使わなくても制御フローを実現できる」なら、 「今のコードをgotoでも作れる」のではないか 7
  3. 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 オペコード スパゲッティ
  4. こんな感じのやつをアレコレする話です • これを変換していきましょう!! • 変換できると嬉しいですよね! 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 オペコード スパゲッティ
  5. オペコードの出力 • オペコードの出力には色々な方法がある • PHPをそのまま使う: php -d opcache.opt_debug_level=0x10000 • 拡張を入れる:

    VLD • コマンドを使う: phpdbgコマンド • 今回はphpdbgコマンドを使っている • スクリプトの実行をせずにオペコードだけ取り出したかったので 21
  6. オペコードの出力 • 素朴にexec() 22 $command = sprintf( '%s -n -p\\*

    %s 2>&1', escapeshellcmd($this->phpdbgPath), escapeshellarg($phpFilePath), ); $output = []; $returnCode = 0; exec($command, $output, $returnCode);
  7. オペコードのフレームごとに `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 ɾ ɾ
  8. 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
  9. 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)
  10. 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
  11. • 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(
  12. • オペコードごとに対応する変換器を判定して、変換を実行 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}"); }
  13. 例: ComparisonTranslator 34 class ComparisonTranslator implements TranslatorInterface { public function

    supports(Opcode $opcode): bool { return isset(self::OPCODE_OPERATORS[$opcode->opcode]); }
  14. 例: 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' => '===', ];
  15. 例: 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};"; } }
  16. 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:
  17. "=", [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の結果 (説明用に整形しています)
  18. "=", [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: 行番号
  19. "=", [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行目」のトークンを探して
  20. "=", [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 の開始位置を見つけたら
  21. "=", [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], そこから「配列の中身」を抜き出す = "[" と "]" の間のトークン
  22. "=", [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]
  23. おまけ • なんやかんやで、 Slimを使ったアプリケーションの スパゲッティ化に成功しました • →のコードが変換元(32行) => 53,957行 •

    github.com/o0h/the-spaghetti-dream-gallery を見てみてください • 僕は怖くて中身を見ていません • ただし変換前後のコードに対して 同じ内容のE2Eテストはパス済み(thx runn!) 56