diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-10-30 19:05:32 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-10-30 19:09:51 +0900 |
| commit | b7388cdc04ee6385fea7cd5e6e2fdc57b1845624 (patch) | |
| tree | 60f9e166542592017d9ef1ddb7d79f24832f78fc | |
| parent | 9fbc7168397ff77c015e34c863d4631335132d38 (diff) | |
| download | phpstudy-169-slides-main.tar.gz phpstudy-169-slides-main.tar.zst phpstudy-169-slides-main.zip | |
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | slide.pdf | bin | 0 -> 281149 bytes | |||
| -rw-r--r-- | slide.saty | 316 | ||||
| -rw-r--r-- | src/.editorconfig | 5 | ||||
| -rw-r--r-- | src/a.php | 13 | ||||
| -rw-r--r-- | src/b.php | 286 |
7 files changed, 618 insertions, 5 deletions
@@ -1,2 +1 @@ -/slide.pdf /slide.satysfi-aux @@ -1 +1 @@ -https://phpstudy.connpass.com/event/TODO/ +https://phpstudy.connpass.com/event/332735/ diff --git a/slide.pdf b/slide.pdf Binary files differnew file mode 100644 index 0000000..acdd51b --- /dev/null +++ b/slide.pdf @@ -26,6 +26,7 @@ let-block +code-block-php source = +code-printer?:( CodePrinter.make-config CodeSyntax.php CodeTheme.iceberg-light |> CodePrinter.set-number-fun CodeDesign.number-fun-null + |> CodePrinter.set-basic-font-size 16pt )(source); > @@ -40,10 +41,10 @@ document '< +make-title(| title = { - |TODO + |PHPでPHPを作る(縮小版) |}; author = {|nsfisis (いまむら)|}; - date = {|第TODO回PHP勉強会@東京|}; + date = {|第169回PHP勉強会@東京|}; |); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -66,8 +67,317 @@ document '< %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +frame{PHPでPHPを作る}< + +listing{ + * 簡単な言語処理系は簡単に作れる + * PHPでも言語処理系を作れる + } + > + + +frame{PHPでPHPを作る}< + +listing{ + * 簡単な言語処理系は簡単に作れる + * PHPでも言語処理系を作れる + * PHPで、FizzBuzz が動くだけの PHP 処理系を実装してみよう + } + > + + +frame{今回の制約}< + +listing{ + * なるべく言語処理系特有の知識・専門用語を使わずに実装・説明してみる + * 今回のプログラムがギリギリ動かせるくらいのミニマムで愚直な実装を目指す + * 今回出てこない用語: 文法クラス、構文解析、AST、VM、バイトコード等 + } + > + + +frame{動かすプログラム(FizzBuzz)}< + +code-block-php(`<?php +for ($i = 1; $i <= 100; $i++) { + if ($i % 15 === 0) { + echo "FizzBuzz"; + } elseif ($i % 3 === 0) { + echo "Fizz"; + } elseif ($i % 5 === 0) { + echo "Buzz"; + } else { + echo $i; + } + echo "\n"; +}`); + > + + +frame{全体の流れ}< + +enumerate{ + * ソースコードという一かたまりの文字列を意味のある最小単位(単語)に分割する + * 前から順番に単語を見ていき、それに対応した処理をおこなう + } + > + + +frame{単語への分割}< + +code-block-php(`<?php +for ($i = 1; $i <= 100; $i++) { + if ($i % 15 === 0) { + echo "FizzBuzz"; + } elseif ($i % 3 === 0) { + echo "Fizz"; + } elseif ($i % 5 === 0) { + echo "Buzz"; + } else { + echo $i; + } + echo "\n"; +}`); + > + + +frame{単語への分割}< + +code-block-php(`function split_into_words(string $input): array { + $i = 0; + $result = []; + while ($i < strlen($input)) { + $first = $input[$i]; + if ($first === '<') { + // ... + } + } + return $result; +}`); + > + + +frame{単語への分割}< + +code-block-php(`while ($i < strlen($input)) { + $first = $input[$i]; + if ($first === '<') { + ... + } else if ($first === '(') { + $result[] = Word::LeftParen; + $i += 1; + } else if ($first === ')') { + $result[] = Word::RightParen; + $i += 1; + } else if (...) { + ... + } +}`); + > + + +frame{単語への分割}< + +code-block-php(#` ... + } else if (ctype_space($first)) { + $i += 1; + } else if (ctype_digit($first)) { + $j = $i; + while (ctype_digit($input[$j])) { + $j += 1; + } + $result[] = (int) substr($input, $i, $j - $i); + $i = $j; + } else if (...) { + ...`); + > + + +frame{単語への分割}< + +code-block-php(#` ... + } else if (ctype_alpha($first)) { + $j = $i; + while (ctype_alpha($input[$j])) { + $j += 1; + } + $result[] = match (substr($input, $i, $j - $i)) { + 'echo' => Word::Echo_, + 'for' => Word::For_, + 'if' => Word::If_, + 'elseif' => Word::ElseIf_, + 'else' => Word::Else_, + }; + $i = $j; + } else if (...) { + ...`); + > + + +frame{単語への分割}< + +code-block-php(`<?php +for ($i = 1; $i <= 100; $i++) { + if ($i % 15 === 0) { + echo "FizzBuzz"; + } elseif ($i % 3 === 0) { + echo "Fizz"; + } elseif ($i % 5 === 0) { + echo "Buzz"; + } else { + echo $i; + } + echo "\n"; +}`); + > + + +frame{単語への分割}< + +code-block-php(`[ + Word::PhpTag, + Word::For_, + Word::LeftParen, + new Variable("i"), + Word::Assign, + 1, + Word::Semicolon, + ..., +]`); + > + + +frame{次のステップ}< + +p{単語の配列を前から順番に見ていき、対応する処理をおこなう} + > + + +frame{実行する}< + +code-block-php(`class Php + private array $words; + private int $position; + private array $variables; + + public function __construct(array $words) { + $this->words = $words; + $this->position = 0; + $this->variables = []; + } +}`); + > + + +frame{実行する}< + +code-block-php(`class Php { + ... + public function runPhp() { + $this->expectWord(Word::PhpTag); + $this->runStatements(); + } + private function expectWord(Word $expected_word) { + if ($this->words[$this->position] !== $expected_word) { + throw new RuntimeException(...); + } + $this->position += 1; + } +}`); + > + + +frame{実行する}< + +code-block-php(`private function runStatements() { + while (true) { + $first = $this->words[$this->position] ?? null; + if ($first === Word::For_) { + $this->runForStatement(); + } else if ($first === Word::If_) { + $this->runIfStatement(); + } else if ($first === Word::Echo_) { + $this->runEchoStatement(); + } else { + break; + } + } +}`); + > + + +frame{実行する}< + +code-block-php(`private function runEchoStatement() { + $this->position += 1; // skip 'echo' + $value = $this->calculateExpression(); + echo $value; + $this->expectWord(Word::Semicolon); +}`); + > + + +frame{実行する}< + +code-block-php(`private function calculateExpression() { + $left_hand_side = $this->getNextWord(); + while (true) { + $next_word = $this->words[$this->position]; + if ($next_word === Word::StrictlyEqual) { + $this->position += 1; // skip '===' + $right_hand_side = $this->getNextWord(); + $left_hand_side = $left_hand_side === $right_hand_side; + } else if (...) { + ... + } else { + return $left_hand_side; + } + } +}`); + > + + +frame{実行する}< + +code-block-php(`private function runIfStatement() { + $this->position += 1; // skip 'if' + $this->expectWord(Word::LeftParen); + $condition = $this->calculateExpression(); + $this->expectWord(Word::RightParen); + $this->expectWord(Word::LeftBrace); + $this->runStatements(); + $this->expectWord(Word::RightBrace); +}`); + > + + +frame{実行する}< + +code-block-php(`private function runIfStatement(bool $doRun = true) { + $this->position += 1; // skip 'if' + $this->expectWord(Word::LeftParen); + $condition = $this->calculateExpression($doRun); + $this->expectWord(Word::RightParen); + $this->expectWord(Word::LeftBrace); + $this->runStatements($doRun && $condition); + $this->expectWord(Word::RightBrace); +}`); + > + + +frame{実行する}< + +code-block-php(`private function runEchoStatement(bool $doRun = true) { + $this->position += 1; // skip 'echo' + $value = $this->calculateExpression($doRun); + if ($doRun) { + echo $value; + } + $this->expectWord(Word::Semicolon); +}`); + > + + +frame{実行する}< + +code-block-php(`for ($i = 1; $i <= 100; $i++) { + ... +}`); + > + + +frame{実行する}< + +code-block-php(#` $this->calculateExpression($doRun); + $condition_position = $this->position; + while (true) { + $condition_result = $this->calculateExpression($doRun); + $update_position = $this->position; + if (!$condition_result) { + $this->calculateExpression(doRun: false); + $this->runStatements(doRun: false); + break; + } + $this->calculateExpression(doRun: false); + $this->runStatements($doRun); + $this->position = $update_position; + $this->calculateExpression($doRun); + $this->position = $condition_position; + } +}`); + > + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +frame{まとめ}< - +p{TODO} + +listing{ + * 簡単な言語処理系は簡単に作れる + * 今回の実装は286行 + * 昔の PHP は、これと大して変わらないくらいのアーキテクチャだった + } + > + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +frame{宣伝}< + +p{PHPカンファレンス小田原2025 (4/12)} + +p{11/4 プロポーザル募集開始!} + +p{「匿名プロポーザル」を実施します (詳細は note 記事へ)} > > diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..819fb4e --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.php] +indent_style = space +indent_size = 4 diff --git a/src/a.php b/src/a.php new file mode 100644 index 0000000..1003341 --- /dev/null +++ b/src/a.php @@ -0,0 +1,13 @@ +<?php +for ($i = 1; $i <= 100; $i++) { + if ($i % 15 === 0) { + echo "FizzBuzz"; + } elseif ($i % 3 === 0) { + echo "Fizz"; + } elseif ($i % 5 === 0) { + echo "Buzz"; + } else { + echo $i; + } + echo "\n"; +} diff --git a/src/b.php b/src/b.php new file mode 100644 index 0000000..6746a58 --- /dev/null +++ b/src/b.php @@ -0,0 +1,286 @@ +<?php + +declare(strict_types=1); + +enum Word { + case PhpTag; + case LessThan; + case LeftParen; + case RightParen; + case LeftBrace; + case RightBrace; + case Semicolon; + case Modulo; + case Increment; + case StrictlyEqual; + case Assign; + case Echo_; + case For_; + case If_; + case ElseIf_; + case Else_; +} + +readonly class Variable { + public function __construct( + public string $name, + ) {} +} + +function split_into_words(string $input): array { + $i = 0; + $result = []; + + while ($i < strlen($input)) { + $first = $input[$i]; + if ($first === '<') { + $second = $input[$i + 1]; + if ($second === '=') { + $result[] = Word::LessThan; + $i += 2; + } else { + $result[] = Word::PhpTag; + $i += 5; + } + } else if ($first === '(') { + $result[] = Word::LeftParen; + $i += 1; + } else if ($first === ')') { + $result[] = Word::RightParen; + $i += 1; + } else if ($first === '{') { + $result[] = Word::LeftBrace; + $i += 1; + } else if ($first === '}') { + $result[] = Word::RightBrace; + $i += 1; + } else if ($first === ';') { + $result[] = Word::Semicolon; + $i += 1; + } else if ($first === '%') { + $result[] = Word::Modulo; + $i += 1; + } else if ($first === '+') { + $result[] = Word::Increment; + $i += 2; + } else if ($first === '=') { + $second = $input[$i + 1]; + if ($second === '=') { + $result[] = Word::StrictlyEqual; + $i += 3; + } else { + $result[] = Word::Assign; + $i += 1; + } + } else if (ctype_space($first)) { + $i += 1; + } else if (ctype_digit($first)) { + $j = $i; + while (ctype_digit($input[$j])) { + $j += 1; + } + $result[] = (int) substr($input, $i, $j - $i); + $i = $j; + } else if (ctype_alpha($first)) { + $j = $i; + while (ctype_alpha($input[$j])) { + $j += 1; + } + $result[] = match (substr($input, $i, $j - $i)) { + 'echo' => Word::Echo_, + 'for' => Word::For_, + 'if' => Word::If_, + 'elseif' => Word::ElseIf_, + 'else' => Word::Else_, + }; + $i = $j; + } else if ($first === '$') { + $i += 1; + $j = $i; + while (ctype_alpha($input[$j])) { + $j += 1; + } + $result[] = new Variable(substr($input, $i, $j - $i)); + $i = $j; + } else if ($first === '"') { + $i += 1; + $j = $i; + $s = ''; + while ($input[$j] !== '"') { + if ($input[$j] === '\\') { + $j += 1; + if ($input[$j] === 'n') { + $s .= "\n"; + } else { + $s .= $input[$j]; + } + } else { + $s .= $input[$j]; + } + $j += 1; + } + $result[] = $s; + $i = $j + 1; + } + } + + return $result; +} + +class Php { + private array $words; + private int $position; + private array $variables; + + public function __construct(array $words) { + $this->words = $words; + $this->position = 0; + $this->variables = []; + } + + public function runPhp(): void { + $this->expectWord(Word::PhpTag); + $this->runStatements(); + } + + private function runStatements(bool $doRun = true): void { + while (true) { + $first = $this->words[$this->position] ?? null; + if ($first === Word::For_) { + $this->runForStatement($doRun); + } else if ($first === Word::If_) { + $this->runIfStatement($doRun); + } else if ($first === Word::Echo_) { + $this->runEchoStatement($doRun); + } else { + break; + } + } + } + + private function runForStatement(bool $doRun = true): void { + $this->position += 1; // skip 'for' + $this->expectWord(Word::LeftParen); + $this->calculateExpression($doRun); + $this->expectWord(Word::Semicolon); + $condition_position = $this->position; + while (true) { + $condition_result = $this->calculateExpression($doRun); + $this->expectWord(Word::Semicolon); + $update_position = $this->position; + if (!$condition_result) { + $this->skipExpression(); + $this->expectWord(Word::RightParen); + $this->expectWord(Word::LeftBrace); + $this->skipProgram(); + $this->expectWord(Word::RightBrace); + break; + } + $this->skipExpression(); + $this->expectWord(Word::RightParen); + $this->expectWord(Word::LeftBrace); + $this->runStatements($doRun); + $this->expectWord(Word::RightBrace); + $this->position = $update_position; + $this->calculateExpression($doRun); + $this->position = $condition_position; + } + } + + private function runIfStatement(bool $doRun = true): void { + $this->position += 1; // skip 'if' or 'elseif' + $this->expectWord(Word::LeftParen); + $condition = $this->calculateExpression($doRun); + $this->expectWord(Word::RightParen); + $this->expectWord(Word::LeftBrace); + $this->runStatements($doRun && $condition); + $this->expectWord(Word::RightBrace); + $next_word = $this->words[$this->position] ?? null; + if ($next_word === Word::ElseIf_) { + $this->runIfStatement($doRun && !$condition); + } else if ($next_word === Word::Else_) { + $this->position += 1; // skip 'else' + $this->expectWord(Word::LeftBrace); + $this->runStatements($doRun && !$condition); + $this->expectWord(Word::RightBrace); + } + } + + private function runEchoStatement(bool $doRun = true): void { + $this->position += 1; // skip 'echo' + $value = $this->calculateExpression($doRun); + if ($doRun) { + echo $value; + } + $this->expectWord(Word::Semicolon); + } + + private function calculateExpression(bool $doRun = true): int|string|bool|null { + $left_hand_side = $this->getNextWord(); + while (true) { + $next_word = $this->words[$this->position]; + if ($next_word === Word::Assign) { + $this->position += 1; // skip '=' + $right_hand_side = $this->getNextWord(); + if ($doRun) { + assert($left_hand_side instanceof Variable); + $left_hand_side = $this->variables[$left_hand_side->name] = $right_hand_side; + } + } else if ($next_word === Word::LessThan) { + $this->position += 1; // skip '<=' + $right_hand_side = $this->getNextWord(); + $left_hand_side = $this->convertWordToValue($left_hand_side) <= $this->convertWordToValue($right_hand_side); + } else if ($next_word === Word::StrictlyEqual) { + $this->position += 1; // skip '===' + $right_hand_side = $this->getNextWord(); + $left_hand_side = $this->convertWordToValue($left_hand_side) === $this->convertWordToValue($right_hand_side); + } else if ($next_word === Word::Modulo) { + $this->position += 1; // skip '%' + $right_hand_side = $this->getNextWord(); + $left_hand_side = $this->convertWordToValue($left_hand_side) % $this->convertWordToValue($right_hand_side); + } else if ($next_word === Word::Increment) { + $this->position += 1; // skip '++' + if ($doRun) { + $left_hand_side = $this->variables[$left_hand_side->name] = $this->variables[$left_hand_side->name] + 1; + } + } else { + return $this->convertWordToValue($left_hand_side); + } + } + } + + private function getNextWord(): int|string|Variable { + $word = $this->words[$this->position]; + $this->position += 1; + return $word; + } + + private function convertWordToValue(int|string|bool|Variable $word): int|string|bool|null { + if ($word instanceof Variable) { + return $this->variables[$word->name]; + } else { + return $word; + } + } + + private function skipExpression(): void { + $this->calculateExpression(doRun: false); + } + + private function skipProgram(): void { + $this->runStatements(doRun: false); + } + + private function expectWord(Word $expected_word): void { + if ($this->words[$this->position] !== $expected_word) { + throw new RuntimeException("Expected $expected_word at position $this->position"); + } + $this->position += 1; + } +} + +$input = file_get_contents('./a.php'); +$words = split_into_words($input); + +$php = new Php($words); +$php->runPhp(); |
