aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-10-30 19:05:32 +0900
committernsfisis <nsfisis@gmail.com>2024-10-30 19:09:51 +0900
commitb7388cdc04ee6385fea7cd5e6e2fdc57b1845624 (patch)
tree60f9e166542592017d9ef1ddb7d79f24832f78fc
parent9fbc7168397ff77c015e34c863d4631335132d38 (diff)
downloadphpstudy-169-slides-main.tar.gz
phpstudy-169-slides-main.tar.zst
phpstudy-169-slides-main.zip
Add slidesHEADmain
-rw-r--r--.gitignore1
-rw-r--r--README.md2
-rw-r--r--slide.pdfbin0 -> 281149 bytes
-rw-r--r--slide.saty316
-rw-r--r--src/.editorconfig5
-rw-r--r--src/a.php13
-rw-r--r--src/b.php286
7 files changed, 618 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index 3a108ac..19780ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-/slide.pdf
/slide.satysfi-aux
diff --git a/README.md b/README.md
index 5bb5b4d..517f7cd 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-https://phpstudy.connpass.com/event/TODO/
+https://phpstudy.connpass.com/event/332735/
diff --git a/slide.pdf b/slide.pdf
new file mode 100644
index 0000000..acdd51b
--- /dev/null
+++ b/slide.pdf
Binary files differ
diff --git a/slide.saty b/slide.saty
index 528e69b..f69691e 100644
--- a/slide.saty
+++ b/slide.saty
@@ -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();