diff options
Diffstat (limited to 'services/blog/content/posts')
82 files changed, 8169 insertions, 0 deletions
diff --git a/services/blog/content/posts/2021-03-05/my-first-post.dj b/services/blog/content/posts/2021-03-05/my-first-post.dj new file mode 100644 index 00000000..147683cf --- /dev/null +++ b/services/blog/content/posts/2021-03-05/my-first-post.dj @@ -0,0 +1,106 @@ +--- +[article] +uuid = "6e9c71fd-bc8d-43ce-99c5-13d9f5b87ed2" +title = "My First Post" +description = "これはテスト投稿です。これはテスト投稿です。これはテスト投稿です。" +tags = [] + +[[article.revisions]] +date = "2021-03-05" +remark = "公開" + +[[article.revisions]] +date = "2025-05-12" +remark = "ジェネレータやスタイルをテストするためのコンテンツを追加" +--- +{#test} +# Test + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +occaecat cupidatat non proident, sunt in culpa qui officia deserunt +mollit anim id est laborum. + +{#sec-1} +# Section level 1 + +{#sec-2} +## Section level 2 + +{#sec-3} +### Section level 3 + +{#sec-4} +#### Section level 4 + +{#sec-5} +##### Section level 5 + +* list item 1 +* list item 2 + + * list item 2.a + * list item 2.b + +* list item 3 +* list item 3 + +1. list item 1 +1. list item 2 + + 1. list item 2.a + 1. list item 2.b + +1. list item 3 +1. list item 3 + +* [ ] list item 1 +* [ ] list item 2 + + * [ ] list item 2.a + * [ ] list item 2.b + +* [ ] list item 3 +* [ ] list item 3 + +> blockquote + +--- + +```ruby +puts "Hello, World!" +``` + +*emph* _strong_ + +{=highlighted=} + +https://example.com + +[example link](https://example.com) + +H~2~O + +2^64^ + +'foo' "bar" + +`code` + +{+inserted+} {-deleted-} + +footenote. [^foo] + +[^foo]: foo bar + +::: note +hoge piyo +::: + +| name | age | +|--------|----:| +| Taro | 10 | +| Hanako | 20 | diff --git a/services/blog/content/posts/2021-03-30/phperkaigi-2021.dj b/services/blog/content/posts/2021-03-30/phperkaigi-2021.dj new file mode 100644 index 00000000..fac9f1ed --- /dev/null +++ b/services/blog/content/posts/2021-03-30/phperkaigi-2021.dj @@ -0,0 +1,177 @@ +--- +[article] +uuid = "3fbe0b8c-216e-48f6-b905-c0d361b94542" +title = "PHPerKaigi 2021" +description = "2021-03-26 から 2021-03-28 にかけて開催された、PHPerKaigi 2021 に参加した。" +tags = [ + "conference", + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2021-03-30" +remark = "公開" + +[[article.revisions]] +date = "2025-04-09" +remark = "それぞれの発表に関するメモ部分を削除し、感想のみに" +--- +{#report} +# PHPerKaigi 2021 参加レポ + +2021-03-26 から 2021-03-28 +にかけて開催された、[PHPerKaigi 2021](https://phperkaigi.jp/2021/) +に一般参加者として参加した。 +弊社[デジタルサーカス株式会社](https://www.dgcircus.com/) +(今年1月から勤務) +はダイヤモンドスポンサーとなっており、スポンサー枠のチケットを使わせていただいた。 + +このようなカンファレンスには初めて参加するのでかねてより心待ちにしていたのだが、生憎2日目から体調を崩してしまい、この記事も途中までとなっている。まだ見ていないセッションも多いが、ひとまず現時点での参加レポを書いておく。 + +発表はトラック A、B に分かれていたのだが、今回はすべて A +トラックを視聴している (切り替えるのが面倒だっただけ)。 + +{#day-0} +## Day 0 前夜祭 (2021/03/27) + +{#1730-a} +### 17:30 [A] LAMPをこじらせてサーバーレスに乗り遅れたPHPerがLambdaに入門してみる + +AWS Lambda のような Function as a Service +はマイクロサービス化における一つの到達点に思えるのだが、これを使って実際に +web サービスを作る具体的なイメージがまだ見えない (注: すべて for me +として書いている)。 + +PHP on AWS Lambda があれだけ簡単に動かせるのには驚いた。 + +勝手に AWS Lambda だとフットプリントの軽さが求められそう (= PHP + +Laravel などでは動かなさそう) +だという先入観を持っていたのだが、この発表のデモによればそうでもないらしい。 + +{#1810-a} +### 18:10 [A] 大規模サイトにおけるSEO観点でのURL設計 + +SEO (Search Engine Optimization) +は大して知らないので新鮮な話が多かった。その分語れることも少ない……。 + +{#1850-a} +### 18:50 [A] PHPerでもわかる!実践Webアクセシビリティ + +つい最近 WAI-ARIA +についての記事を読んだばかりだったので個人的にタイムリーな話題だった。(あまりこの言葉を使いたくないのだが) +いわゆる「健常者」にとって、こうした問題を普段の生活の中で意識するのは難しい。だからこそ情報へのアンテナは張っておくようにしたい。 + +{#1930-a} +### 19:30 [A] PHP でファイルシステムを作ろう + +PHP で FUSE + +個人的に楽しみだった発表。 + +期待通りの興味深い発表だった。FUSE +自体も今回の発表で知ったのだが、これ本体の実装を見るのも面白そうだ。 +この発表を聞きながらファイルシステムにマウントできそうなものを考えていたのだが、およそ木構造をしているものすべてと言えそうだ +(ハンマーしか持っていないと云々)。何かできそうだがなかなか思いつかない。 + +{#day-1} +## Day 1 (2021/03/27) + +{#1050-a} +### 10:50 [A] 実践ATDD 〜TDDから更に歩みを進めたソフトウェア開発へ〜 + +User Acceptance Test (UAT) +くらいの規模になると個人開発・趣味開発では触れない領域なので、大いに勉強になった。スライドに添付されている資料が相当に充実していたので、これを読むのが本番といった様相すら感じる。 +高レベルテストの自動化は現在のプロジェクトでも感じており、自動化のチャンスは伺っている。とはいえセッションでも指摘されているように自動化することにコストがかかりすぎる領域があるのも事実で、そのバランスが難しい。 + +{#1150-a} +### 11:50 [A] 静的型解析を用いた大規模レガシーコードのリファクタリング計画 + +型のある世界で生きてきた身として大いに楽しみにしていた発表。 + +昨今、動的型付き言語での型宣言・型アノテーション・型ヒントの導入が相次いでいる。長らく静的型付き言語を書いてきた私からすると、ようやく気づいたかといったところだが、ともかく型を導入する言語が増えてきた。 +今のプロジェクトでも新しく追加するコードには型をつけるよう努めているが、どうしても古いコードには型がついていない。個人的には型のないコードに対してどう型を自動的に付けるかという点に興味があり、その点で +Ruby の typeprof には注目している。 + +{#1310-a} +### 13:10 [A] 目的に沿ったDocumentation as Codeをいかにして実現していくか + +この発表も以前から非常に楽しみにしていた。 + +ドキュメントの管理は現プロジェクトでも課題と感じている。作られた当初は正しくても、実態と乖離していくのを止めるのは困難を極める。全体的に興味深い発表だったが、特にスタックトレースからのドキュメント生成というアイデアに惹かれるものを感じた。スタックトレースという実態と不可分な +(乖離しない) +情報を起点にするのは理にかなっている。問題はトレースをいつ、どう取るかだろうか。それを自動化しなければ、実態との乖離が避けられないだろう。 + +{#1410-a} +### 14:10 [A] PHPで学ぶ、セッションの基本と応用 + +全体的に基本的な話だったので特に触れない。Cookie +やセッションの話としては非常に分かりやすくまとめられていたので、知らない人が学ぶにはいい教材だろう。 + +{#1450-a} +### 14:50 [A] PHP8になった今の時代に、PHPの「エラー」「例外」そして「Error」をおさらいしておこう + +PHP を学んでいる途中の私としては、今まさに聞きたい発表だった (現時点で +PHP を書き始めてから 4ヶ月ほどになる)。 + +個人的に例外やエラーを最もうまく扱っているのは Go、Swift、Rust、Haskell +などのエラーを「値として」扱う言語だと思っている。try-catch +は通常の処理フローを完全に壊してしまう上、構文としても重すぎる。値としてのエラー通知は +C言語時代への回帰ともいえるが、その頃と異なるのはエラーを暗黙のうちに握り潰すことがないということだ。これらの言語は型を持っており、静的に検証ができる +(C のそれはまともな型付けではない。念のため)。 + +PHP +のように、すでに例外が言語システムに根ざしている言語ではどうすればよいか。この場合も同じく静的検証の力を借りることになるだろう。 + +{#1530-a} +### 15:30 [A] Laravel のメール認証の内部実装を掘り下げる + +Laravel +の知識がない私にはまったくついていけなかった。また、個人的にタイトルがややミスリーディングに感じた。 + +{#1610-a} +### 16:10 [A] ブラウザから始めるgRPC 〜 gRPC-WebにPHPを添えて + +(発表の中でもまさに同じことをおっしゃっていたが) PHP +以外の方が向いているだろう、というのが第一の感想である。gRPC +はそれ自体というよりも Protobuf +というエコシステムに乗れることのメリットが大きいと感じる。そのエコシステムにうまく乗れない時点で、うーんという感じ。 + +{#day-2} +## Day 2 (2021/03/28) + +冒頭に書いた通り、2日目から体調が悪くまともに聴けていない。途中までは頭痛を我慢しつつ見ていたのだが、まともに入ってこなかった。 + +残念ではあるが、いずれにせよ見られていない発表は他にもあるので、今週末にでもまとめて見ようと思う。 + +{#comments} +## 全体の感想 + +Day 2 +にほとんど参加できなかったのは残念だが、イベント自体は大変楽しく、また興味深いものであった。自分がまったく知らない領域の話を聞けるのはこうしたイベントならではだと感じる。オンライン開催ゆえ現地に行く必要がなく、気軽に参加できたのも +(特に初参加者として) 嬉しいポイントだった。 + +今回、雑談/登壇者への質問等向けに Discord +サーバもあったのだが、こちらは参加こそしたものの ROM +のままになってしまった。発表に1ウィンドウ、メモを書くのに1ウィンドウ、Discord +表示に +1ウィンドウで私にはもう脳のリソースとディスプレイのスペースが追いつかなかった +(さらにいうと Zoom +でアンカンファレンスもやっていたようだ。こちらはまったく参加していない)。 + +1つ個人的な反省点としては、一つ一つのセッションを真剣に聞き過ぎたというものがある。もっと適当に聞いておけばよかった。これだけだと大変語弊があるのだが、言い方を変えると、Discord +しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、後から見返せる発表やスライドに注力してしまった、ということだ。発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。 +まあ初カンファレンスだし、とお茶を濁しておこう。 + +さて、カンファレンスで一つ気になったことがある。それは、Discord +という書き込み場所が増えたことでニコ生のコメントの流量が吸い取られてしまったのではないか、という点だ。ニコニコだけ見ていると過疎っているかのように見えた発表も、Discord +の方では盛り上がっている、というのを何度か見かけた。ニコニコのコメント方式は盛り上がりを如実に反映するが、逆もまたしかり。Discord +があったこと自体はプラスだったと思うが、この点はマイナスだったのではないかと感じる。 + +---------------- + +最後になりましたが、毎年の PHPerKaigi +開催にご尽力されている皆様、スピーカーの皆様、楽しい3日間でした。ありがとうございました! +(ずっと常体で書いてしまったのでいきなり仏頂面から笑顔になったようで気持ち悪い) + +ではまた来年。 diff --git a/services/blog/content/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.dj b/services/blog/content/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.dj new file mode 100644 index 00000000..94690092 --- /dev/null +++ b/services/blog/content/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes.dj @@ -0,0 +1,113 @@ +--- +[article] +uuid = "69863d75-ef21-42db-b743-5958f7c86827" +title = "【C++】 属性構文の属性名にはキーワードが使える" +description = "C++ の属性構文の属性名には、キーワードが使える。ネタ記事。" +tags = [ + "cpp", + "cpp17", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/94090937bcf860cfa93b +::: + +タイトル落ち。まずはこのコードを見て欲しい。 + +```cpp +#include <iostream> + +[[alignas]] [[alignof]] [[and]] [[and_eq]] [[asm]] [[auto]] [[bitand]] +[[bitor]] [[bool]] [[break]] [[case]] [[catch]] [[char]] [[char16_t]] +[[char32_t]] [[class]] [[compl]] [[const]] [[const_cast]] [[constexpr]] +[[continue]] [[decltype]] [[default]] [[delete]] [[do]] [[double]] +[[dynamic_cast]] [[else]] [[enum]] [[explicit]] [[export]] [[extern]] [[false]] +[[final]] [[float]] [[for]] [[friend]] [[goto]] [[if]] [[inline]] [[int]] +[[long]] [[mutable]] [[namespace]] [[new]] [[noexcept]] [[not]] [[not_eq]] +[[nullptr]] [[operator]] [[or]] [[or_eq]] [[override]] [[private]] +[[protected]] [[public]] [[register]] [[reinterpret_cast]] [[return]] [[short]] +[[signed]] [[sizeof]] [[static]] [[static_assert]] [[static_cast]] [[struct]] +[[switch]] [[template]] [[this]] [[thread_local]] [[throw]] [[true]] [[try]] +[[typedef]] [[typeid]] [[typename]] [[union]] [[unsigned]] +[[virtual]] [[void]] [[volatile]] [[wchar_t]] [[while]] [[xor]] [[xor_eq]] +// [[using]] +int main() { + std::cout << "Hello, World!" << std::endl; +} +``` + +コンパイラのバージョン + +``` +$ clang++ –version Apple clang version 11.0.0 +(clang-1100.0.33.8) Target: x86_64-apple-darwin19.6.0 Thread model: +posix InstalledDir: +/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin +``` + +コンパイルコマンド (C17指定) + +``` +$ clang –std=c++17 hoge.cpp +``` + +この記事から得られるものはこれ以上ないので以下は蛇足になる。 + +別件で [cppreference.com の identifier のページ](https://en.cppreference.com/w/cpp/language/identifiers)を読んでいた時、次の文が目に止まった。 + +> * the identifiers that are keywords cannot be used for other purposes; +> +> * The only place they can be used as non-keywords is in an attribute-token. (e.g. [[private]] is a valid attribute) (since C++11) + +キーワードでも属性として指定する場合は非キーワードとして使えるらしい。 +実際にやってみる。 + +同サイトの [keywords のページ](https://en.cppreference.com/w/cpp/keyword) +から一覧を拝借し、上のコードが出来上がった (C++17 +においてキーワードでないものなど、一部省いている)。 大量の警告 (unknown +attribute \`〇〇' ignored) +がコンパイラから出力されるが、コンパイルできる。 + +上のコードでは `[[using]]` をコメントアウトしているが、これは `using` +キーワードのみ属性構文の中で意味を持つからであり、このコメントアウトを外すとコンパイルに失敗する。 + +```cpp +// using の例 +[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文 +``` + +C++17 の仕様も見てみる (正確には標準化前のドラフト)。 + +引用元: https://timsong-cpp.github.io/cppwp/n4659/dcl.attr#grammar-4 + +> If a keyword or an alternative token that satisfies the syntactic +> requirements of an identifier is contained in an attribute-token, it is +> considered an identifier. + +「`identifier` の構文上の要件を満たすキーワードまたは代替トークンが +`attribute-token` に含まれている場合、`identifier` +とみなされる」とある。どうやら間違いないようだ。 + +ところで、代替トークン (alternative token) とは `and` (`&`) や `bitor` +(`|`) などのことだが、`identifier` +の構文上の要件を満たさないような代替トークンなどあるのか? +疑問に思って調べたところ、代替トークンという語にはダイグラフも含まれるらしい +(参考: +[同ドラフト](https://timsong-cpp.github.io/cppwp/n4659/lex.digraph) ) + +* `<%` → `{` +* `%>` → `}` +* `<:` → `[` +* `:>` → `]` +* `%:` → `#` +* `%:%:` → `##` + +「`identifier` +の構文上の要件を満たさないような代替トークン」はこれらが当てはまると思われる。 + +調べた感想: 字句解析器か構文解析器が辛そう diff --git a/services/blog/content/posts/2021-10-02/python-unbound-local-error.dj b/services/blog/content/posts/2021-10-02/python-unbound-local-error.dj new file mode 100644 index 00000000..88d03151 --- /dev/null +++ b/services/blog/content/posts/2021-10-02/python-unbound-local-error.dj @@ -0,0 +1,70 @@ +--- +[article] +uuid = "e1aff84c-d6d4-4dea-bc45-9c41e6445006" +title = "【Python】 クロージャとUnboundLocalError: local variable 'x' referenced before assignment" +description = "Python における UnboundLocalError の理由と対処法。" +tags = [ + "python", + "python3", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/5d733703afcb35bbf399 +::: + +本記事は Python 3.7.6 の動作結果を元にして書かれている。 + +Python でクロージャを作ろうと、次のようなコードを書いた。 + +```python +def f(): + x = 0 + def g(): + x += 1 + g() + +f() +``` + +関数 `g` から 関数 `f` のスコープ内で定義された変数 `x` を参照し、それに +1 を足そうとしている。 これを実行すると `x += 1` +の箇所でエラーが発生する。 + +> UnboundLocalError: local variable \`x' referenced before assignment + +local変数 `x` が代入前に参照された、とある。これは、`f` の `x` +を参照するのではなく、新しく別の変数を `g` 内に作ってしまっているため。 +前述のコードを宣言と代入を便宜上分けて書き直すと次のようになる。`var` +を変数宣言のための構文として擬似的に利用している。 + +```python +# 注: var は正しい Python の文法ではない。上記参照のこと +def f(): + var x # f の local変数 'x' を宣言 + x = 0 # x に 0 を代入 + def g(): # f の内部関数 g を定義 + var x # g の local変数 'x' を宣言 + # たまたま f にも同じ名前の変数があるが、それとは別の変数 + x += 1 # x に 1 を加算 (x = x + 1 の糖衣構文) + # 加算する前の値を参照しようとするが、まだ代入されていないためエラー + g() +``` + +当初の意図を表現するには、次のように書けばよい。 + +```python +def f(): + x = 0 + def g(): + nonlocal x ## (*) + x += 1 + g() +``` + +`(*)` のように、`nonlocal` を追加する。これにより一つ外側のスコープ (`g` +の一つ外側 = `f`) で定義されている `x` を探しに行くようになる。 diff --git a/services/blog/content/posts/2021-10-02/ruby-detect-running-implementation.dj b/services/blog/content/posts/2021-10-02/ruby-detect-running-implementation.dj new file mode 100644 index 00000000..653b7dcd --- /dev/null +++ b/services/blog/content/posts/2021-10-02/ruby-detect-running-implementation.dj @@ -0,0 +1,74 @@ +--- +[article] +uuid = "e1456a50-4fc6-42ef-89f3-8be78e01da13" +title = "【Ruby】 自身を実行している処理系の種類を判定する" +description = "Ruby には複数の実装があるが、自身を実行している処理系の種類をスクリプト上からどのように判定すればよいだろうか。" +tags = [ + "ruby", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/74d7ffeeebc51b20d791 +::: + +Ruby +という言語には複数の実装があるが、それらをスクリプト上からどのようにして +programmatically に見分ければよいだろうか。 + +`Object` クラスに定義されている `RUBY_ENGINE` +という定数がこの用途に使える。 + +参考: +[Object::RUBY_ENGINE](https://docs.ruby-lang.org/ja/latest/method/Object/c/RUBY_ENGINE.html) + +上記ページの例から引用する: + +```shell-session +$ ruby-1.9.1 -ve 'p RUBY_ENGINE' +ruby 1.9.1p0 (2009-03-04 revision 22762) [x86_64-linux] +"ruby" +$ jruby -ve 'p RUBY_ENGINE' +jruby 1.2.0 (ruby 1.8.6 patchlevel 287) (2009-03-16 rev 9419) [i386-java] +"jruby" +``` + +それぞれの処理系がどのような値を返すかだが、stack overflow +に良い質問と回答があった。 + +[What values for RUBY_ENGINE correspond to which Ruby implementations?](https://stackoverflow.com/a/9894232) より引用: + +> ``` +> | RUBY_ENGINE | Implementation | +> |:-----------:|:------------------| +> | <undefined> | MRI < 1.9 | +> | 'ruby' | MRI >= 1.9 or REE | +> | 'jruby' | JRuby | +> | 'macruby' | MacRuby | +> | 'rbx' | Rubinius | +> | 'maglev' | MagLev | +> | 'ironruby' | IronRuby | +> | 'cardinal' | Cardinal | +> ``` + +なお、この質問・回答は +2014年になされたものであり、値は変わっている可能性がある。MRI (aka +CRuby) については執筆時現在 (2020/12/8) も `'ruby'` +が返ってくることを確認済み。 + +この表にない主要な処理系として、 [mruby](https://mruby.org) は `'mruby'` +を返す。 + +[mruby 該当部分のソース](https://github.com/mruby/mruby/blob/ed29d74bfd95362eaeb946fcf7e865d80346b62b/include/mruby/version.h#L32-L35) より引用: + +{filename="version.h"} +```c +/* + * Ruby engine. + */ +#define MRUBY_RUBY_ENGINE "mruby" +``` diff --git a/services/blog/content/posts/2021-10-02/ruby-then-keyword-and-case-in.dj b/services/blog/content/posts/2021-10-02/ruby-then-keyword-and-case-in.dj new file mode 100644 index 00000000..82d6d9cc --- /dev/null +++ b/services/blog/content/posts/2021-10-02/ruby-then-keyword-and-case-in.dj @@ -0,0 +1,233 @@ +--- +[article] +uuid = "87455008-fe5b-49bf-af5a-b875264f8326" +title = "【Ruby】 then キーワードと case in" +description = "Ruby 3.0 で追加される case in 構文と、then キーワードについて。" +tags = [ + "ruby", + "ruby3", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/787a8cf888a304497223 +::: + +{#tl-dr} +# TL; DR + +`case` - `in` によるパターンマッチング構文でも、`case` - `when` +と同じように `then` が使える (場合によっては使う必要がある)。 + +{#what-is-then-keyword} +# `then` とは + +使われることは稀だが、Ruby では `then` +がキーワードになっている。次のように使う: + +```ruby +if cond then + puts "Y" +else + puts "N" +end +``` + +このキーワードが現れうる場所はいくつかあり、`if`、`unless`、`rescue`、`case` +構文がそれに当たる。 上記のように、何か条件を書いた後 `then` +を置き、式がそこで終了していることを示すマーカーとして機能する。 + +```ruby +# Example: + +if x then + a +end + +unless x then + a +end + +begin + a +rescue then + b +end + +case x +when p then + a +end +``` + +{#why-then-is-usually-unnecessary} +# なぜ普段は書かなくてもよいのか + +普通 Ruby のコードで `then` +を書くことはない。なぜか。次のコードを実行してみるとわかる。 + +```ruby +if true puts 'Hello, World!' end +``` + +次のような構文エラーが出力される。 + +``` +20:1: syntax error, unexpected local variable or method, expecting `then' or ';' or '\n' +if true puts 'Hello, World!' end + ^~~~ +20:1: syntax error, unexpected `end', expecting end-of-input +...f true puts 'Hello, World!' end +``` + +二つ目のメッセージは無視して一つ目を読むと、`then` か `;` +か改行が来るはずのところ変数だかメソッドだかが現れたことによりエラーとなっているようだ。 + +ポイントは改行が `then` (や `;`) の代わりとなることである。`true` +の後に改行を入れてみる。 + +```ruby +if true +puts 'Hello, World!' end +``` + +無事 Hello, World! と出力されるようになった。 + +{#why-then-or-linebreak-is-needed} +# なぜ `then` や `;` や改行が必要か + +なぜ `then` や `;` や改行 (以下 「`then` 等」) +が必要なのだろうか。次の例を見てほしい: + +```ruby +if a b end +``` + +`then` も `;` +も改行もないのでエラーになるが、これは条件式がどこまで続いているのかわからないためだ。 +この例は二通りに解釈できる。 + +```ruby +# a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価 +if a then +b +end +``` + +```ruby +# a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、 +# その結果が truthy なら何もしない +if a(b) then +end +``` + +`then` 等はこの曖昧性を排除するためにあり、条件式は `if` から `then` +等までの間にある、ということを明確にする。 C系の `if` 後に来る `(`/`)` +や、Python の `:`、Rust/Go/Swift などの `{` も同じ役割を持つ。 + +Ruby の場合、プログラマーが書きやすいよう改行でもって `then` +が代用できるので、ほとんどの場合 `then` は必要ない。 + +{#then-in-case-in} +# `case` - `in` における `then` + +ようやく本題にたどり着いた。来る Ruby 3.0 では `case` と `in` +キーワードを使ったパターンマッチングの構文が入る予定である。この構文でもパターン部との区切りとして +`then` 等が必要になる。 (現在の) Ruby には formal +な形式での文法仕様は存在しないので、yacc の定義ファイルを参照した (yacc +の説明は省略)。 + +https://github.com/ruby/ruby/blob/221ca0f8281d39f0dfdfe13b2448875384bbf735/parse.y#L3961-L3986 + +{filename="parse.y"} +```yacc +p_case_body : keyword_in +{ + SET_LEX_STATE(EXPR_BEG|EXPR_LABEL); + p->command_start = FALSE; + $<ctxt>1 = p->ctxt; + p->ctxt.in_kwarg = 1; + $<tbl>$ = push_pvtbl(p); +} +{ + $<tbl>$ = push_pktbl(p); +} +p_top_expr then +{ + pop_pktbl(p, $<tbl>3); + pop_pvtbl(p, $<tbl>2); + p->ctxt.in_kwarg = $<ctxt>1.in_kwarg; +} +compstmt +p_cases +{ + /*%%%*/ + $$ = NEW_IN($4, $7, $8, &@$); + /*% %*/ + /*% ripper: in!($4, $7, escape_Qundef($8)) %*/ +} +; +``` + +簡略版: + +```yacc +p_case_body : keyword_in p_top_expr then compstmt p_cases +; +``` + +ここで、`keyword_in` は文字通り `in`、`p_top_expr` +はいわゆるパターン、`then` は `then` +キーワードのことではなく、この記事で `then` 等と呼んでいるもの、つまり +`then` キーワード、`;`、改行のいずれかである。 + +これにより、`case` - `when` による従来の構文と同じように、`then` +等をパターンの後ろに挿入すればよいことがわかった。つまり次の3通りのいずれかになる: + +```ruby +case x +in 1 then a +in 2 then b +in 3 then c +end + +case x +in 1 + a +in 2 + b +in 3 + c +end + +case x +in 1; a +in 2; b +in 3; c +end +``` + +ところで、`p_top_expr` には `if` による guard clause +が書けるので、その場合は `if` - `then` と似たような見た目になる。 + +```ruby +case x +in 0 then a +in n if n < 0 then b +in n then c +end +``` + +{#outro} +# まとめ + +* `if` や `case` の条件の後ろには `then`、`;`、改行のいずれかが必要 + + * 通常は改行しておけばよい + +* 3.0 で入る予定の `case` - `in` でも `then` 等が必要になる +* Ruby の構文を正確に知るには (現状) `parse.y` を直接読めばよい diff --git a/services/blog/content/posts/2021-10-02/rust-where-are-primitive-types-from.dj b/services/blog/content/posts/2021-10-02/rust-where-are-primitive-types-from.dj new file mode 100644 index 00000000..9fa61d56 --- /dev/null +++ b/services/blog/content/posts/2021-10-02/rust-where-are-primitive-types-from.dj @@ -0,0 +1,206 @@ +--- +[article] +uuid = "91c61980-c262-4e8d-89b0-4304e7f6d644" +title = "Rust のプリミティブ型はどこからやって来るか" +description = "Rust のプリミティブ型は予約語ではなく普通の識別子である。どのようにこれが名前解決されるのかを調べた。" +tags = [ + "rust", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/9a429432258bbcd6c565 +::: + +{#intro} +# 前置き + +Rust +において、プリミティブ型の名前は予約語でない。したがって、次のコードは合法である。 + +```rust +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +struct bool; +struct char; +struct i8; +struct i16; +struct i32; +struct i64; +struct i128; +struct isize; +struct u8; +struct u16; +struct u32; +struct u64; +struct u128; +struct usize; +struct f32; +struct f64; +struct str; +``` + +では、普段単に `bool` と書いたとき、この `bool` +は一体どこから来ているのか。rustc のソースを追ってみた。 + + 前提知識: 一般的なコンパイラの構造、用語。`rustc` そのものの知識は不要 + (というよりも筆者自身がよく知らない) + +{#code-reading} +# 調査 + +調査に使用したソース (調査時点での最新 master) + +https://github.com/rust-lang/rust/tree/511ed9f2356af365ad8affe046b3dd33f7ac3c98 + +どのようにして調べるか。rustc +の構造には詳しくないため、すぐに当たりをつけるのは難しい。 + +大雑把な構造としては、`compiler` フォルダ以下に `rustc_*` +という名前のクレートが数十個入っている。これがどうやら `rustc` +コマンドの実装部のようだ。 + +`rustc` はセルフホストされている (= `rustc` 自身が Rust で書かれている) +ので、`bool` や `char` +などで適当に検索をかけてもノイズが多すぎて話にならない。 +しかし、お誂え向きなことに `i128`/`u128` +というコンパイラ自身が使うことがなさそうな型が存在するのでこれを使って +`git grep` してみる。 + +``` +$ git grep "\bi128\b" | wc # i128 +165 1069 15790 + +$ git grep "\bu128\b" | wc # u128 +293 2127 26667 + +$ git grep "\bbool\b" | wc # cf. bool の結果 +3563 23577 294659 +``` + +165 +程度であれば探すことができそうだ。今回は、クレート名を見ておおよその当たりをつけた。 + +``` +$ git grep "\bi128\b" +... +rustc_resolve/src/lib.rs: table.insert(sym::i128, Int(IntTy::I128)); +... +``` + +`rustc_resolve` +というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。 + +{filename="rustc_resolve/src/lib.rs"} +```rust +/// Interns the names of the primitive types. +/// +/// All other types are defined somewhere and possibly imported, but the primitive ones need +/// special handling, since they have no place of origin. +struct PrimitiveTypeTable { + primitive_types: FxHashMap<Symbol, PrimTy>, +} + +impl PrimitiveTypeTable { + fn new() -> PrimitiveTypeTable { + let mut table = FxHashMap::default(); + + table.insert(sym::bool, Bool); + table.insert(sym::char, Char); + table.insert(sym::f32, Float(FloatTy::F32)); + table.insert(sym::f64, Float(FloatTy::F64)); + table.insert(sym::isize, Int(IntTy::Isize)); + table.insert(sym::i8, Int(IntTy::I8)); + table.insert(sym::i16, Int(IntTy::I16)); + table.insert(sym::i32, Int(IntTy::I32)); + table.insert(sym::i64, Int(IntTy::I64)); + table.insert(sym::i128, Int(IntTy::I128)); + table.insert(sym::str, Str); + table.insert(sym::usize, Uint(UintTy::Usize)); + table.insert(sym::u8, Uint(UintTy::U8)); + table.insert(sym::u16, Uint(UintTy::U16)); + table.insert(sym::u32, Uint(UintTy::U32)); + table.insert(sym::u64, Uint(UintTy::U64)); + table.insert(sym::u128, Uint(UintTy::U128)); + Self { primitive_types: table } + } +} +``` + +これは初めに列挙したプリミティブ型の一覧と一致している。doc comment +にも、 + +``` +All other types are defined somewhere and possibly imported, but the +primitive ones need special handling, since they have no place of +origin. +``` + +とある。次はこの struct +の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。 + +```rust +/// This resolves the identifier `ident` in the namespace `ns` in the current lexical scope. +/// (略) +fn resolve_ident_in_lexical_scope( + &mut self, + mut ident: Ident, + ns: Namespace, + // (略) +) -> Option<LexicalScopeBinding<'a>> { + // (略) + + if ns == TypeNS { + if let Some(prim_ty) = self.primitive_type_table.primitive_types.get(&ident.name) { + let binding = + (Res::PrimTy(*prim_ty), ty::Visibility::Public, DUMMY_SP, ExpnId::root()) + .to_name_binding(self.arenas); + return Some(LexicalScopeBinding::Item(binding)); + } + } + + None +} +``` + +関数名や doc comment が示している通り、この関数は識別子 (identifier, +ident) を現在のレキシカルスコープ内で解決 (resolve) する。 +`if ns == TypeNS` のブロック内では、`primitive_type_table` (上記の +`PrimitiveTypeTable::new()` で作られた変数) に含まれている識別子 +(`bool`、`i32` など) +かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。 + +なお、`ns` は「名前空間」を示す変数である。Rust +における名前空間はC言語におけるそれとほとんど同じで、今探している名前が関数名/変数名なのか型なのかマクロなのかを区別している。この +`if` +は、プリミティブ型に解決されるのは型を探しているときだけだ、と言っている。 + +重要なのは、これが `resolve_ident_in_lexical_scope()` +の最後に書かれている点である。つまり、最初に挙げたプリミティブ型の識別子は、「名前解決の最終段階で」、「他に同名の型が見つかっていなければ」プリミティブ型として解決される。 + +動作がわかったところで、例として次のコードを考える。 + +```rust +#![allow(non_camel_case_types)] + +struct bool; + +fn main() { + let _: bool = bool; +} +``` + +ここで `main()` の `bool` は `struct bool` +として解決される。なぜなら、プリミティブ型の判定をする前に `bool` +という名前の別の型が見つかるからだ。 + +{#outro} +# まとめ + +Rust +のプリミティブ型は予約語ではない。名前解決の最終段階で特別扱いされ、他に同名の型が見つかっていなければ対応するプリミティブ型に解決される。 diff --git a/services/blog/content/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre.dj b/services/blog/content/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre.dj new file mode 100644 index 00000000..a97337d0 --- /dev/null +++ b/services/blog/content/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre.dj @@ -0,0 +1,131 @@ +--- +[article] +uuid = "44171f75-c312-4c92-9927-3d260e162175" +title = "【Vim】 autocmd events の BufWrite/BufWritePre の違い" +description = "Vim の autocmd events における BufWrite/BufWritePre がどう違うのかを調べた結果、違いはないことがわかった。" +tags = [ + "vim", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/79ab4db8564032de0b25 +::: + +{#tl-dr} +# TL; DR + +違いはない。ただのエイリアス。 + +{#code-reading} +# 調査記録 + +Vim の autocmd events には似通った名前のものがいくつかある。大抵は +`:help` +に説明があるが、この記事のタイトルにある2つを含めた以下のイベントには、その違いについて説明がない。 + +* `BufRead`/`BufReadPost` +* `BufWrite`/`BufWritePre` +* `BufAdd`/`BufCreate` + +このうち、`BufAdd`/`BufCreate` に関しては、`:help BufCreate` に + +> The BufCreate event is for historic reasons. + +とあり、おそらくは `BufAdd` +のエイリアスであろうということがわかる。他の2組も同様ではないかと予想されるが、確認のため +vim と neovim のソースコードを調査した。 + +ソースコードへのリンク + +* [vim (調査時点での master branch)](https://github.com/vim/vim/tree/8e6be34338f13a6a625f19bcef82019c9adc65f2) +* [neovim (上に同じ)](https://github.com/neovim/neovim/tree/71d4f5851f068eeb432af34850dddda8cc1c71e3) + +{#vim} +## vim のソースコード + +以下は、autocmd events +の名前と内部で使われている整数値とのマッピングを定義している箇所である。見ての通り、上でエイリアスではないかと述べた3組には、それぞれ同じ内部値が使われている。 + +https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L85-L86 + +{filename="src/autocmd.c"} +```c +{"BufAdd", EVENT_BUFADD}, +{"BufCreate", EVENT_BUFADD}, +``` + +https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L95-L97 + +{filename="src/autocmd.c"} +```c +{"BufRead", EVENT_BUFREADPOST}, +{"BufReadCmd", EVENT_BUFREADCMD}, +{"BufReadPost", EVENT_BUFREADPOST}, +``` + +https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L103-L105 + +{filename="src/autocmd.c"} +```c +{"BufWrite", EVENT_BUFWRITEPRE}, +{"BufWritePost", EVENT_BUFWRITEPOST}, +{"BufWritePre", EVENT_BUFWRITEPRE}, +``` + +{#neovim} +## neovim のソースコード + +neovim の場合でも同様のマッピングが定義されているが、こちらの場合は Lua +で書かれている。以下にある通り、はっきり `aliases` と書かれている。 + +https://github.com/neovim/neovim/blob/71d4f5851f068eeb432af34850dddda8cc1c71e3/src/nvim/auevents.lua#L119-L124 + +{filename="src/nvim/auevents.lua"} +```lua +aliases = { + BufCreate = 'BufAdd', + BufRead = 'BufReadPost', + BufWrite = 'BufWritePre', + FileEncoding = 'EncodingChanged', +}, +``` + +ところで、上では取り上げなかった `FileEncoding` だが、これは +`:help FileEncoding` にしっかりと書いてある。 + +{filename=":help FileEncoding"} +``` + *FileEncoding* +FileEncoding Obsolete. It still works and is equivalent + to |EncodingChanged|. +``` + +{#outro} +# まとめ + +記事タイトルについて言えば、どちらも変わらないので好きな方を使えばよい。あえて言えば、次のようになるだろう。 + +* `BufAdd`/`BufCreate` + + * → `BufCreate` は歴史的な理由により ("for historic reasons") 存在しているため、新しい方 (`BufAdd`) を使う + +* `BufRead`/`BufReadPost` + + * → `BufReadPre` との対称性のため、あるいは `BufWritePost` との対称性のため `BufReadPost` を使う + +* `BufWrite`/`BufWritePre` + + * → `BufWritePost` との対称性のため、あるいは `BufReadPre` との対称性のため `BufWritePre` を使う + +* `FileEncoding`/`EncodingChanged` + + * → `FileEncoding` は "Obsolete" と明言されているので、`EncodingChanged` を使う + +ところでこの調査で知ったのだが、`BufRead` と `BufWrite` +は上にある通り発火するタイミングが「後」と「前」で対称性がない。可能なら +`Pre`/`Post` 付きのものを使った方が分かりやすいだろう。 diff --git a/services/blog/content/posts/2021-10-02/vim-swap-order-of-selected-lines.dj b/services/blog/content/posts/2021-10-02/vim-swap-order-of-selected-lines.dj new file mode 100644 index 00000000..1cd070eb --- /dev/null +++ b/services/blog/content/posts/2021-10-02/vim-swap-order-of-selected-lines.dj @@ -0,0 +1,163 @@ +--- +[article] +uuid = "665de47e-0ed6-405e-ad30-81c3c4592d45" +title = "Vimで選択した行の順番を入れ替える" +description = "Vim で選択した行の順番を入れ替える方法。" +tags = [ + "vim", +] + +[[article.revisions]] +date = "2021-10-02" +remark = "Qiita から移植" +--- +::: note +この記事は Qiita から移植してきたものです。 +元 URL: https://qiita.com/nsfisis/items/4fefb361d9a693803520 +::: + +{#tl-dr} +# TL; DR + +```vim +" License: Public Domain + +command! -bar -range=% + \ Reverse + \ keeppatterns <line1>,<line2>g/^/m<line1>-1 +``` + +{#version} +# バージョン情報 + +`:version` の一部 + +> VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Jan 26 2020 11\:30\:30) macOS +> version Included patches: 1-148 Huge version without GUI. + +{#existing-solution} +# よく紹介されている手法 + +{#external-commands} +## `tac` / `tail` + +`tac` や `tail -r` などの外部コマンドを `!` +を使って呼び出し、置き換える。 + +> :h v_! + +`tac` コマンドや `tail` の `-r` +オプションは環境によって利用できないことがあり、複数の環境を行き来する場合に採用しづらい + +{#global-command} +## `:g/^/m0` + +こちらは外部コマンドに頼らず、Vim の機能のみを使う。`g` は `:global` +コマンドの、`m` は `:move` コマンドの略 + +`:global` コマンドは `:[range]global/{pattern}/[command]` +のように使い、`[range]` で指定された範囲の行のうち、`{pattern}` +で指定された検索パターンにマッチする行に対して、順番に `[command]` +で指定された Ex コマンドを呼び出す。 + +> :h :global + +`:move` コマンドは `[range]:move {address}` のように使い、`[range]` +で指定された範囲の行を `{address}` で指定された位置に移動させる。 + +> :h :move + +`:g/^/m0` のように組み合わせると、「すべての行を1行ずつ +0行目(1行目の上)に動かす」という動きをする。これは確かに行の入れ替えになっている。 + +なお、`:g/^/m0` は全ての行を入れ替えるが、`:N,Mg/^/mN-1` とすることで +N行目から +M行目を処理範囲とするよう拡張できる。手でこれを入力するわけにはいかないので、次のようなコマンドを用意する。 + +```vim +command! -bar -range=% + \ Reverse + \ <line1>,<line2>g/^/m<line1>-1 +``` + +これは望みの動作をするが、実際に実行してみると全行がハイライトされてしまう。次節で詳細を述べる。 + +{#problem-of-global-command} +# `:g/^/m0` の問題点 + +`:global` +コマンドは各行に対してマッチングを行う際、現在の検索パターンを上書きしてしまう。`^` +は行の先頭にマッチするため、結果として全ての行がハイライトされてしまう。`'hlsearch'` +オプションを無効にしている場合その限りではないが、その場合でも直前の検索パターンが失われてしまうと +`n` コマンドなどの際に不便である。 + +> :h @/ + +{#solution} +# 解決策 + +{editat="2020-09-28" operation="追記"} +::: edit +より簡潔な方法を見つけたので次節に追記した。 +::: + +前述した `:Reverse` コマンドの定義を少し変えて、次のようにする: + +```vim +function! s:reverse_lines(from, to) abort + execute printf("%d,%dg/^/m%d", a:from, a:to, a:from - 1) +endfunction + +command! -bar -range=% + \ Reverse + \ call <SID>reverse_lines(<line1>, <line2>) +``` + +実行しているコマンドが変わったわけではないが、関数呼び出しを経由するようにした。これだけで前述の問題が解決する。 + +この理由は、ユーザー定義関数を実行する際は検索パターンが一度保存され、実行が終了したあと復元されるため。結果として検索パターンが +`^` で上書きされることがなくなる。 + +Vim のヘルプから該当箇所を引用する (強調は筆者による)。 + +> :h autocmd-searchpat +> +> *Autocommands do not change the current search patterns.* Vim saves the +> current search patterns before executing autocommands then restores them +> after the autocommands finish. This means that autocommands do not +> affect the strings highlighted with the 'hlsearch' option. + +これは autocommand +の実行に関しての記述だが、これと同じことがユーザー定義関数の実行時にも適用される。このことは +`:nohlsearch` のヘルプにある。同じく該当箇所を引用する +(強調は筆者による)。 + +> :h :nohlsearch +> +> (略) This command doesn’t work in an autocommand, because the +> highlighting state is saved and restored when executing autocommands +> |autocmd-searchpat|. *Same thing for when invoking a user function.* + +この仕様により、`:g/^/m0` +の呼び出しをユーザー定義関数に切り出すことで上述の問題を解決できる。 + +{#solution-revised} +# 解決策 (改訂版) + +{editat="2020-09-28" operation="追記"} +::: edit +より簡潔な方法を見つけたため追記する。 +::: + +```vim +command! -bar -range=% + \ Reverse + \ keeppatterns <line1>,<line2>g/^/m<line1>-1 +``` + +まさにこのための Exコマンド、`:keeppatterns` +が存在する。`:keeppatterns {command}` +のように使い、読んで字の如く、後ろに続く +Exコマンドを「現在の検索パターンを保ったまま」実行する。はるかに分かりやすく意図を表現できる。 + +> :h :keeppatterns diff --git a/services/blog/content/posts/2022-04-09/phperkaigi-2022-tokens.dj b/services/blog/content/posts/2022-04-09/phperkaigi-2022-tokens.dj new file mode 100644 index 00000000..39565833 --- /dev/null +++ b/services/blog/content/posts/2022-04-09/phperkaigi-2022-tokens.dj @@ -0,0 +1,492 @@ +--- +[article] +uuid = "f4985d54-0907-4449-8101-0fcd382f9e02" +title = "PHPerKaigi 2022 トークン問題の解説" +description = "PHPerKaigi 2022 で私が作成した PHPer チャレンジ問題を解説する。" +tags = [ + "conference", + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2022-04-09" +remark = "公開" + +[[article.revisions]] +date = "2022-04-16" +remark = "2問目、3問目の解説を追加、1問目に加筆" +--- +{#intro} +# はじめに + +本日開始された [PHPerKaigi 2022](https://phperkaigi.jp/2022/) の PHPer +チャレンジにおいて、弊社 +[デジタルサーカス株式会社](https://www.dgcircus.com/) の問題を +3問作成した。この記事では、これらの問題の解説をおこなう。 + +リポジトリはこちら: https://github.com/nsfisis/PHPerKaigi2022-tokens + +{#q1-brainfuck} +# 第1問 brainf_ck.php + +ソースコードはこちら。実行には PHP 8.1 以上が必要なので注意。 + +{filename="brainf_ck.php"} +```php +<?php + +declare(strict_types=0O1); + +namespace Dgcircus\PHPerKaigi\Y2022; + +/** + * @todo + * Run this program to acquire a PHPer token. + */ + +https://creativecommons.org/publicdomain/zero/1.0/ + +\error_reporting(~+!'We are hiring!'); + +$z = fn($f) => (fn($x) => $f(fn(...$xs) => $x($x)(...$xs)))(fn($x) => $f(fn(...$xs) => $x($x)(...$xs))); +$id = \spl_object_id(...); +$put = fn($c) => \printf('%c', $c); +$mm = fn($p, $n) => new \ArrayObject(\array_fill(+!![], $n, $p)); + +$👉 = fn($m, $p, $b, $e, $mp, $pc) => [++$mp, ++$pc]; +$👈 = fn($m, $p, $b, $e, $mp, $pc) => [--$mp, ++$pc]; +$👍 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, ++$m[$mp]]; +$👎 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, --$m[$mp]]; +$📝 = fn($m, $p, $b, $e, $mp, $pc) => [$mp, ++$pc, $put($m[$mp])]; +$🤡 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) { + +!![] => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) { + $b => $loop(++$pc, ++$n), + $e => $n === +!![] ? ++$pc : $loop(++$pc, --$n), + default => $loop(++$pc, $n), + })($pc, -![])], + default => [$mp, ++$pc], +}; +$🎪 = fn($m, $p, $b, $e, $mp, $pc) => match ($m[$mp]) { + +!![] => [$mp, ++$pc], + default => [$mp, $z(fn($loop) => fn($pc, $n) => match ($id($p[$pc])) { + $e => $loop(--$pc, ++$n), + $b => $n === +!![] ? $pc+![] : $loop(--$pc, --$n), + default => $loop(--$pc, $n), + })($pc, -![])], +}; +$🐘 = fn($p) => $z(fn($loop) => fn($m, $p, $b, $e, $mp, $pc) => + isset($p[$pc]) && $loop($m, $p, $b, $e, ...($p[$pc]($m, $p, $b, $e, $mp, $pc))) +)($mm(+!![], +(![].![])), $p, $id($🤡), $id($🎪), +!![], +!![]); + +$🐘([ + $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, + $🤡, + $👉, $👍, $👍, $👍, + $👉, $👍, $👍, $👍, $👍, $👍, + $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, + $👉, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, $👍, + $👈, $👈, $👈, $👈, $👎, + $🎪, + $👉, $👍, $👍, $👍, $👍, $👍, $📝, + $👎, $👎, $📝, + $👉, $👎, $👎, $👎, $📝, + $👉, $👎, $👎, $👎, $📝, + $👎, $👎, $📝, + $👎, $📝, + $👈, $📝, + $👉, $👉, $👎, $👎, $📝, + $👍, $👍, $👍, $👍, $👍, $👍, $👍, $📝, + $👈, $👎, $👎, $👎, $👎, $📝, + $👈, $📝, + $👉, $👍, $👍, $📝, + $👉, $👎, $📝, + $👈, $📝, +]); +``` + +この問題は、単に適切なバージョンの PHP で動かせばトークンが得られる。 + +{#commentary} +## 解説 + +{#emoji} +### 絵文字 + +まず目につくのは大量の絵文字だろう。 PHP +は識別子に使用できる文字の範囲が広く、絵文字も使うことができる。 + +{#brainfuck} +### プログラム全体 + +Brainf\*ck のインタプリタとプログラムになっている。 Brainf\*ck +とは、難解プログラミング言語のひとつであり、ここで説明するよりも +Wikipedia の該当ページを読んだ方がよい。 + +https://ja.wikipedia.org/wiki/Brainfuck + +なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。 + +``` ++ + + + + + + + + + +[ + > + + + + > + + + + + + > + + + + + + + + + + + + + > + + + + + + + + + + + < < < < - +] +> + + + + + . +- - . +> - - - . +> - - - . +- - . +- . +< . +> > - - . ++ + + + + + + . +< - - - - . +< . +> + + . +> - . +< . +``` + +実行結果はこちら: https://ideone.com/22VWmb + +それぞれの絵文字で表された関数が、各命令に対応している。 + +* `$👉`: `>` +* `$👈`: `<` +* `$👍`: `+` +* `$👎`: `-` +* `$📝`: `.` +* `$🤡`: `[` +* `$🎪`: `]` + +`,` (入力) に対応する関数はない +(このプログラムでは使わないので用意していない)。 + +なお、`$🐘` はいわゆる main 関数であり、プログラムの実行部分である。 + +{#emoji-selection} +### 絵文字の選択 + +おおよそ意味に合致するよう選んでいるが、`$🤡` と `$🎪` +は弊社デジタルサーカスにちなんでいる。 また、`$🐘` は PHP +のマスコットの象に由来する。 + +{#strict-types} +### strict_types + +`declare` 文の `strict_types` に指定できるのは、`0` か `1` +の数値リテラルだが、 `0x0` や `0b1` のような値も受け付ける。 今回は、PHP +8.1 から追加された、`0O` または `0o` から始まる八進数リテラルを使った。 + +{#url} +### URL + +ソースコードのライセンスを示したこの部分だが、 + +```php +https://creativecommons.org/publicdomain/zero/1.0/ +``` + +完全に合法な PHP のコードである。 `https:` 部分はラベル、`//` +以降は行コメントになっている。 + +{#numbers} +### リテラルなしで数値を生成する + +ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。 +PHP では、型変換を利用することで任意の整数を作り出すことができる。 + +```php +assert(0 === +!![]); +assert(1 === +![]); +assert(2 === ![]+![]); +assert(3 === ![]+![]+![]); +assert(10 === +(![].+!![])); +``` + +`[]` に `!` を適用すると `true` が返ってくる。それに `+` +を適用すると、`bool` から `int` ヘの型変換が走り、`1` が生成される。`10` +はさらにトリッキーだ。まず `1` と `0` を作り、`.` で文字列として結合する +(`'10'`)。これに `+` を適用すると、`string` から `int` +への型変換が走り、`10` が生まれる (コード量に頓着しないなら、`1` を 10 +個足し合わせてももちろん 10 が作れる)。 + +また、`error_reporting` に指定しているのは `-1` である。 これは、`!` +によって文字列を `false` にし、`+` によって `false` を `0` +にし、さらにビット反転して `-1` にしている。 + +{#conditionals} +### `if` 文なしで条件分岐 + +三項演算子ないし `match` 式を使うことで、`if` +を一切書かずに条件分岐ができる。 また、`&&` / `||` も使えることがある。 +遅延評価が不要なケースでは、`[$t, $f][$cond]` +のような形で分岐することもできる。 + +{#loops} +### `while`、`for` 文なしでループ + +不動点コンビネータを使って無名再帰する +(詳しい説明は省略する。これらの単語で検索してほしい)。 ここでは、一般に +Z コンビネータとして知られるものを使った (`$z`)。 + +実際のところ、`$🤡` や `$🎪`、`$🐘` は、一度 Scheme (Lisp の一種) +で書いてから PHP に翻訳する形で記述した。 + +なお、PHP は末尾再帰の最適化をおこなわない (少なくとも今のところは) +ので、 あまりに長い brainf*ck +プログラムを書くとスタックオーバーフローする。 + +{#q2-riddle} +# 第2問 riddle.php + +ソースコードはこちら。実行には PHP 8.0 以上が必要なので注意。 + +{filename="riddle.php"} +```php +<?php + +/********************************************************* + * This program displays a PHPer token. * + * Guess 'N'. * + * * + * Hints: * + * - N itself has no special meaning, e.g., 42, 8128, * + * it is selected at random. * + * - Each element of $token represents a single letter. * + * - One letter consists of 5x5 cells. * + * - Remember, the output is a complete PHPer token. * + * * + * License: * + * https://creativecommons.org/publicdomain/zero/1.0/ * + *********************************************************/ +const N = 0 /* Change it to your answer. */; +assert(0 <= N && N <= 0b11111_11111_11111_11111_11111); + +$token = [ + 0x14B499C, + 0x0BE34CC, 0x01C9C69, + 0x0ECA069, 0x01C2449, 0x0FDB166, 0x01C9C69, + 0x01C1C66, 0x0FC1C47, 0x01C1C66, + 0x10C5858, 0x1E4E3B8, 0x1A2F2F8, +]; +foreach ($token as $x) { + $x = $x ^ N; + + $x = sprintf('%025b', $x); + $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); + $x = implode("\n", str_split($x, length: 5)); + echo "{$x}\n\n"; +} +``` + +さて、この問題はさきほどのように単純に実行しただけでは、謎のブロックが表示されるだけでトークンは得られない。 +トークンを得るためには、ソースコードを読み、定数 `N` +を特定する必要がある。 + +ここでは、私の想定解を解説する。 + +{#code-reading} +## 読解 + +まずはソースコードを読んでいく。 + +```php +$token = [ + // 略 +]; +``` + +数値からなる `$token` があり、各要素をループしている。 + +```php +$x = $x ^ N; +``` + +まずは排他的論理和 (xor) を取り、 + +```php +$x = sprintf('%025b', $x); +``` + +二進数に変換して、 + +```php +$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); +``` + +0 を空白に、1 を `#` にし、 + +```php +$x = implode("\n", str_split($x, length: 5)); +``` + +5文字ごとに区切ったあと、改行で結合している。 + +{#hint} +## ヒント + +次に、ソースコードに書いてあるヒントを読んでいく。 + +* `N` それ自体は、42 や 8128 といったような特別な意味を持たず、ランダムに決められている +* `$token` の各要素は、1文字を表す +* 1文字は 5x5 のセルからなる +* 出力されるのは、完全な PHPer トークンである + +ここで、PHPer トークンは必ず `#` 記号から始まることを思いだすと、 +`$token` の最初の数字 `0x14B499C` は、変換の結果 `#` +になるのではないかと予想される (なお、このことは、リポジトリの README +ファイルに追加ヒントとして書かれている)。 + +{#solve} +## 解く + +ここまでわかれば、あと一歩で解ける。すなわち、`0x14B499C` が `#` +に変換されるような `N` を見つければよい。 + +`N` は高々 + +```php +assert(0 <= N && N <= 0b11111_11111_11111_11111_11111); +``` + +なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。 + +```php +<?php + +$x = 0x14B499C; + +$x = $x ^ N; + +$x = sprintf('%025b', $x); +$x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x); +$x = implode("\n", str_split($x, length: 5)); + +assert($x === +" # # \n" . +"#####\n" . +" # # \n" . +"#####\n" . +" # # "); +``` + +この一連の変換に対する逆変換を考えると、次のようになる。 + +```php +<?php + +$x = +" # # \n" . +"#####\n" . +" # # \n" . +"#####\n" . +" # # "; + +$x = implode('', explode("\n", $x)); +$x = str_replace(search: [' ', '#'], replace: ['0', '1'], subject: $x); +$x = bindec($x); + +$n = $x ^ 0x14B499C; + +echo "N = $n\n"; +``` + +これを実行すると、`N` が得られる。 + +{#q3-toquine} +# 第3問 toquine.php + +ソースコードはこちら。 + +{filename="toquine.php"} +```php +<?php + +// License: https://creativecommons.org/publicdomain/zero/1.0/ +// This is a quine-like program to generate a PHPer token. +// Execute it like this: php toquine.php | php | php | php | ... + +$s = <<<'Q' +<?cuc +// Yvprafr: uggcf://perngvirpbzzbaf.bet/choyvpqbznva/mreb/1.0/ +// Guvf vf n dhvar-yvxr cebtenz gb trarengr n CUCre gbxra. +// Rkrphgr vg yvxr guvf: cuc gbdhvar.cuc | cuc | cuc | cuc | ... +%f$f = %f; +$f = fge_ebg13($f); $kf = [ +%f, +]; +$g = ahyy.snyfr; sbe ($v = 0; $v <= vagqvi(__YVAR__-035,6); ++$v) vs (!vffrg($kf[$v])) oernx; ryfr +$g .= vzcybqr("\a", fge_fcyvg(fge_ercynpr(['0','1'], [' ','##'], fcevags(pue(37) . '025o', $kf[$v])), 012)) . "\a\a"; +$jf = neenl_znc(sa($j) => vzcybqr(', ', $j), neenl_puhax(neenl_znc(sa($k) => fcevags('0k' . pue(37) . '07K', $k), $kf), 10)); +cevags($f, $g, fge_ebg13("<<<'Q'\a{$f}\aQ"), vzcybqr(",\a", $jf)); +Q; +$s = str_rot13($s); $xs = [ +0x0AFABEA, 0x1F294A7, 0x1F2109F, 0x1F294A7, 0x0002800, 0x1F2109F, 0x0117041, 0x1F294A7, 0x1FAD6B5, 0x1F295B7, +0x010FC21, 0x1FAD6B5, 0x1151151, 0x010FC21, 0x1F294A7, 0x1F295B7, 0x1FAD6B5, 0x1F294A7, 0x1F295B7, 0x1F8C63F, +0x1F8C631, 0x1FAD6B5, 0x17AD6BD, 0x17AD6BD, 0x1F8C63F, 0x1F295B7, +]; +$t = null.false; for ($i = 0; $i <= intdiv(__LINE__-035,6); ++$i) if (!isset($xs[$i])) break; else +$t .= implode("\n", str_split(str_replace(['0','1'], [' ','##'], sprintf(chr(37) . '025b', $xs[$i])), 012)) . "\n\n"; +$ws = array_map(fn($w) => implode(', ', $w), array_chunk(array_map(fn($x) => sprintf('0x' . chr(37) . '07X', $x), $xs), 10)); +printf($s, $t, str_rot13("<<<'D'\n{$s}\nD"), implode(",\n", $ws)); +``` + +コメントにもあるとおり、次のようにして実行すれば答えがでてくる。 + +```shell-session +$ php toquine.php | php | php | php | ... +``` + +実際にはもう少しパイプで繋げなければならない。 + +{#commentary} +## 解説 + +{#quine} +### プログラム全体 + +コメントにもあるとおり、これは quine (風) のプログラムになっている。 +Quine +とは、自分のソースコードをそっくりそのまま出力するようなプログラムのことである。 + +このプログラムは、実行すると自身とほとんど同じプログラムを出力する。 +異なるのはトークンになっている部分のみである。 + +{#tokens} +### トークン + +`$xs` がトークンに対応している。変換のロジックは `riddle.php` +とほぼ同じなので省略する。 + +{#states} +### 状態保持 + +トークンの何文字目まで出力したかを、ソースコードを変えずに (quine +なので) 覚えておく必要がある。 +このプログラムでは、トークンが出力されるとソースコードがだんだんと長くなっていくのを利用して、`__LINE__` +から情報を取得している。 + +{#rot-13} +### ROT 13 + +Quine は、素朴に書くとプログラムの一部が 2回記述されてしまう。 +これがあまり美しくないので、`toquine.php` では、ROT 13 +変換を使って難読化した。 + +それにしてもなぜこんなものが標準ライブラリに……。 + +{#outro} +# おわりに + +解いていただいたみなさん、また、難易度調整につきあっていただいた社内のみなさん、ありがとうございました。 + +今回は直前に作りはじめたのもあり、3問だけかつ使い古されたネタばかりになってしまいましたが、 +来年は 5問、より面白い問題を持っていきます。 + +実はもう作りはじめているので、どうか来年もありますように……。 diff --git a/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal.dj b/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal.dj new file mode 100644 index 00000000..59c78e3e --- /dev/null +++ b/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal.dj @@ -0,0 +1,100 @@ +--- +[article] +uuid = "42cf2829-b897-4748-bc22-80dd734a3c09" +title = "term-banner: ターミナルにバナーを表示するツールを書いた" +description = "ターミナルに任意の文字のバナーを表示するためのツールを Go で書いた。" +tags = [] + +[[article.revisions]] +date = "2022-04-24" +remark = "公開" + +[[article.revisions]] +date = "2022-04-27" +remark = "-f オプションについて追記" +--- +{#intro} +# はじめに + +こんなものを作った。 + +``` +$ term-banner 'Hello, World!' 'こんにちは、' '世界!' +``` + + + +コマンドライン引数として渡した文字列をターミナルに大きく表示する。 + +リポジトリはこちら: https://github.com/nsfisis/term-banner + +{#motivation} +# Motivation + +以前、[`big-clock-mode`](https://github.com/nsfisis/big-clock-mode) +という似たようなプログラムを書いた。 これは tmux の `:clock-mode` +コマンドに着想を得たもので、`:clock-mode` +よりも大きく現在時刻を表示する。 + +`big-clock-mode` +を開発したのは、次のようなシチュエーションで使うためである。 +弊社では現在リモートワークが基本だが、web +会議などで画面共有しているときに、休憩を挟んで特定の時刻から再開する、ということがある。 +こういったケースで、画面上に現在の時刻を大きめに表示しておくと、モニタから離れても遠くから時刻がわかるので便利である。 + +それこそタイマアプリか何かを使えばいいのだが、ターミナルに棲むいきものとしては、住処から離れたくないわけだ。 + +しばらく便利に使っていたのだが、ひとつ不満点が出てきた。それは、再開する時刻がいつだったかを覚えておかなければならないということだ。 +どこかにメモしておいてもいいが、せっかくなら現在時刻とともに表示させておきたい。 + +そんなわけで、「任意の文字列をターミナルに表示する」プログラムを書く運びとなった。 +まあ、作らなくても探せばあると思うが、作りたいものは作りたいので知ったことではない。 + +{#program} +# プログラム + +全体の流れは次のようになっている。 + +1. フォントファイルを読み込む +1. コマンドライン引数を Shift-JIS に変換する (フォントが Shift-JIS 基準で並んでいるため) +1. 1文字ずつレンダリングしていく + +`big-clock-mode` が Go 製なので、今回も Go で書いた。 PNG +が標準ライブラリにあったり、Shift-JIS +のエンコーディングが準標準ライブラリにあったりしたのは助かった。 + +フォントファイルは `go:embed` +で実行ファイルに埋め込んでいるので、ビルド後はワンバイナリで動く。 +仕事ではスクリプト言語ばかり書いているが、やはりコンパイル言語はいい。 + +{#font} +# フォント + +フリーの 8x8 +ビットマップフォントである、 [美咲フォント 2021-05-05a 版](https://littlelimit.net/misaki.htm) を使わせていただいた。 + +はじめは自分でポチポチ打っていたのだが、「き」くらいまでやって挫折した。 +同じく 8x8 +で作っていたのだが、平仮名でさえも、この小さなキャンバスにはとても収められない。 + +美咲フォントは、平仮名・片仮名に留まらず、JIS +第一・第二水準の漢字までサポートしている。 +第二水準ともなると一生お目にかかることのない字の方が多いくらいだが、これをこの大きさで書くというのは、もはや芸術の域である。 + +さらに言うと、実のところ美咲フォントは実サイズ 7x7 +で作られており、余白が設けられている。 +これは、単純にそのまま並べても字間・行間を確保できるようにという配慮である。 +おかげでコーディングまで楽になった。 + +ゴシック体と明朝体があったが、私の好みで明朝体の方にした。 +ただ、ゴシック体の方が見やすい気がするので、フォントを選べるように後ほど拡張するかもしれない。 + +{editat="2022-04-27" operation="追記"} +::: edit +`-f` オプションで選べるようにした。 +::: + +{#outro} +# おわりに + +あなたもターミナルに住んでみませんか? diff --git a/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/screenshot.png b/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/screenshot.png Binary files differnew file mode 100644 index 00000000..c527879a --- /dev/null +++ b/services/blog/content/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/screenshot.png diff --git a/services/blog/content/posts/2022-05-01/phperkaigi-2022.dj b/services/blog/content/posts/2022-05-01/phperkaigi-2022.dj new file mode 100644 index 00000000..6758f265 --- /dev/null +++ b/services/blog/content/posts/2022-05-01/phperkaigi-2022.dj @@ -0,0 +1,132 @@ +--- +[article] +uuid = "9211e1fe-bca3-43c4-ba4e-c67d62f3fed0" +title = "PHPerKaigi 2022" +description = "2022-04-09 から 2022-04-11 にかけて開催された、PHPerKaigi 2022 に参加した。" +tags = [ + "conference", + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2022-05-01" +remark = "公開" +--- +{#intro} +# はじめに + +2022-04-09 から 2022-04-11 にかけて開催された、 [PHPerKaigi 2022](https://phperkaigi.jp/2022/) に、 +一般参加者として参加した。 +弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) はダイヤモンドスポンサーとなっており、 +スポンサー枠のチケットを使わせていただいた。 + +昨年のレポートは [こちら](/posts/2021-03-30/phperkaigi-2021) 。 + +{#comments} +# 感想 + +{#great-sessions} +## 厳選おすすめトーク + +多くの素晴らしいトークの中から、特におすすめのものを 5つ選んだ。 +是非聞いてほしい。引用部分は、リンク先プロポーザルから引用している。 + +[予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント](https://fortee.jp/phperkaigi-2022/proposal/ef8cf4ed-63fe-42f8-8145-b3e70054458b) + +> PHP はバージョンを追う毎に型宣言、例外、表明、列挙型などの機能が大幅に強化され、堅牢なコードを書くための機能が充実してきました。それらの機能はどう使うと効果的なのでしょうか。 +> +> 本講演では PHP 8.1 をベースにして、誤りを想定してチェックするのではなく、そもそも誤りにくい設計とはどのようなものか、つまり「予防」の観点を軸足に、堅牢なコードを導くための様々な設計のヒントをご紹介します。 + +[PHPのエラーを理解して適切なエラーハンドリングを学ぼう](https://fortee.jp/phperkaigi-2022/proposal/db00d49e-0dd6-453f-b54b-f731d112f10e) + +> PHPを使ってるとよく遭遇する Fatal error / Parse error / Warning / Notice 理解していますか? +> +> これらのエラー文を理解することで、すぐにエラーの原因に気付き適切に対象できる様になります! +> +> またそれらを理解した上でのエラーハンドリングを学びましょう。 + +[エラー監視とテスト体制への改善作戦](https://fortee.jp/phperkaigi-2022/proposal/4a7e3ded-9134-4919-955c-ec7bf4491c0d) + +> 毎日流れてくるエラーに皆さんはどう向き合ってますか? +> +> エラーを出さない事が一番ですが、完全に塞ぐ事は難しいと考えます。 +> +> サービス運用の中で本番環境から発生するエラー(サーバー・クライアントサイド・サードパーティ起因のエラー)への監視体制と、 +> +> エラー・バグ防御のためチームで行っているテストコード文化づくりの話をします。 + +[ISUCON11のPHP実装は、何を考え、どのようにして作られていたのか](https://fortee.jp/phperkaigi-2022/proposal/6f47daf8-c78f-4fb1-9b99-e9656e6fe7f7) + +> 昨年開催されたISUCON11にて問題(参考実装)のPHPへの移植を担当させていただきました。 +> +> 最終的なソースコードこそシンプルなWebアプリケーションではありますが、その裏には +> +> * 「(私の思う)良い設計」を実現するための意思決定 +> * 「ISUCONの問題」という位置付けに由来する取捨選択 +> * 移植中に遭遇したトラブルとその解決策 +> +> といった文脈や葛藤が存在しています。 +> +> 本発表はそれらを共有することで +> +> * PHPアプリケーションの設計、実装事例として役立ててもらう +> * ISUCONの言語移植に興味を持ってもらう +> * ISUCON問題移植の「実装や設計の練習をする教材」としての可能性を知ってもらう +> +> ことを目的とします。 + +[チームの仕事はまわっていたけど、メンバーはそれぞれモヤモヤを抱えていた話──40名の大規模開発チームで1on1ログを公開してみた](https://fortee.jp/phperkaigi-2022/proposal/5a260e4e-542d-4d82-849d-ef3d6cb7c854) + +> サイボウズの大企業向けグループウェアのGaroon(ガルーン)は、PHPで開発されている20年目の製品です。ガルーン開発チームは日本で40名、ベトナムで50名の計90名ほどのチームになっています。また、コロナ禍でフルリモートでの活動がこの2年ほど継続してきました。 +> +> フルリモートになっても仕事はまわっており、継続的にリリースはしていましたが、一方でお互いの考えていることや感じている問題意識が見えづらくなり、モヤモヤを抱えているメンバーが増えていました。 +> +> このセッションでは、そういう状況で私がチーム外からジョインし、聴き役に徹しながら見える化することで状況を改善していった取り組みを紹介します。同じように大きなチームやリモートワークで難しさを感じている人に、難しさの原因への気づきや取り組みへのヒントがあれば幸いです。 + +{#token-quizzes} +## トークン問題の作成 + +今回は、PHPer チャレンジ用に弊社のトークン問題を 3題作成した。 +こちらについては [別途記事にしている](/posts/2022-04-09/phperkaigi-2022-tokens) ので、そちらを参照されたい。 + +{#phper-challenge} +## PHPer チャレンジ + +[1位](https://fortee.jp/phperkaigi-2022/challenge) になった。 +また、賞品として [Echo Show 15](https://www.amazon.co.jp/dp/B08MQNJC9Z) をいただいた。 + +{#conference} +## カンファレンス全体への感想 + +[去年の参加レポ](/posts/2021-03-30/phperkaigi-2021) では、こんなことを書いた。 + +> 1つ個人的な反省点としては、(中略) Discord しかりアンカンファレンスしかり「このイベントのこの瞬間にしかないコンテンツ」に触れずに、 +> 後から見返せる発表やスライドに注力してしまった、ということだ。 +> 発表の詳細な見直しはあとからできるのだから、今しかできないことを考えるべきだった。 +> まあ初カンファレンスだし、とお茶を濁しておこう。 + +この反省を踏まえ、今年は積極的にほかの場 (公式の Discord サーバや、アンカンファレンス) にも参加した。 +これにより、参加体験の質がはるかに向上した。特に Discord に関しては、登壇者ご本人による補足や、 +質問への回答などがおこなわれる (ことが多い) ため、特別な理由のない限り、発言はしないまでも参加はしておいたほうが良いと思われる。 + +なお、アンカンファレンスについては、1日目の終わりに [トークン問題の解説放送](https://fortee.jp/phperkaigi-2022/unconference/view/d332797a-8921-4706-a7e2-ee72640c9b5e) もおこなった。 + +また、今年はオフラインとオンラインのハイブリッド開催であったが、去年の全オンラインと比べて、オンライン参加の体験が落ちていなかったのは、特筆すべきであろう。 +今年は 3回目のワクチン接種が間に合わなかったこともあり現地参加は見送ったのだが、来年は是非オフラインで参加したい。 + +{#next-year} +# そして来年へ……? + +PHPerKaigi 2023 があるかどうか存じ上げないが、あるとすれば、次の 4つを目標としたい。 + +* プロポーザルを出す +* PHPer チャレンジのトークン問題を 5題作成する +* 現地に行く +* PHPer チャレンジで圧勝する + +---------------- + +最後になりましたが、PHPerKaigi のスタッフ、スポンサー、スピーカーのみなさん、素敵な時間をありがとうございました。 + +ではまた来年。 diff --git a/services/blog/content/posts/2022-08-27/php-conference-okinawa-code-golf.dj b/services/blog/content/posts/2022-08-27/php-conference-okinawa-code-golf.dj new file mode 100644 index 00000000..5701fe4d --- /dev/null +++ b/services/blog/content/posts/2022-08-27/php-conference-okinawa-code-golf.dj @@ -0,0 +1,99 @@ +--- +[article] +uuid = "bb71bb5d-361b-44cb-9753-81d14583d860" +title = "PHP カンファレンス沖縄で出題されたコードゴルフの問題を解いてみた" +description = "PHP カンファレンス沖縄の懇親会 LT で出題されたコードゴルフの問題を解いてみた。" +tags = [ + "conference", + "php", + "phpconokinawa", +] + +[[article.revisions]] +date = "2022-08-27" +remark = "公開" +--- +{#intro} +# はじめに + +本日 [PHP カンファレンス沖縄 2022](https://phpcon.okinawa.jp/) が開催された (らしい)。 + +カンファレンスには参加できなかったものの、懇親会の LT で出題されたコードゴルフの問題が Twitter に流れてきたので、解いてみた。 + +* ツイート: https://twitter.com/m3m0r7/status/1563397620231712772 +* スライド: https://speakerdeck.com/memory1994/php-conference-okinawa-2022-extra?slide=3 + +{#solution} +# 解 + +細かいレギュレーションは不明だったので、勝手に定めた。 + +* コマンドライン引数の第1引数で受けとる +* 結果は標準出力に出す +* コンマの直後にはスペースを1つ置く +* 末尾コンマは禁止 +* 数字でないものは入ってこないものとする +* 負数は入ってこないものとする + +書いたものがこちら: + +```php +[<?php $n=$argv[1];foreach([1e4,5e3,2e3,1e3,500,100,50,10,5,1]as$x)for(;$n>=$x;$n-=$x)$r[]=$x;echo implode(', ',$r??[]);?>] +``` + +しめて 123 バイトとなった (末尾改行を含めずにカウント)。 + +こちらは改行とスペースを追加したバージョン: + +```php +[<?php + +$n = $argv[1]; +foreach ([1e4, 5e3, 2e3, 1e3, 500, 100, 50, 10, 5, 1] as $x) + for (; $n >= $x; $n -= $x) + $r[] = $x; +echo implode(', ', $r ?? []); + +?>] +``` + +{#techniques} +# 使用したテクニック + +{#exponential-notation} +## 指数表記 + +割と多くの言語のゴルフで使えるテクニック。 +`e` を用いた指数表記で、大きな数を短く表す。 +このコードでは `10000`、`5000`、`2000`、`1000` を指数表記している。 + +{#shorten-loop} +## foreach や for の中身を1つの文に + +`foreach`、`for`、`if` などの後ろには、 +通常 `{` を続けて複数の文を連ねるが、中身の文を1つにしてしまえば、`{` と `}` を省略できる。 +C言語などでも使える。 + +{#omit-initialization} +## $r に初期値を入れない + +PHP では、`$r[] = ......` のような配列の末尾に追加する式を実行したとき、 +`$r` が未定義だった場合は `$r` を勝手に定義して空の配列で初期化してくれる。 +これを利用すると、`$r = [];` のような初期化が不要になる。 + +ただし、プログラムに 0 が渡されるとループを一度も回らないので、`$r` が未定義になってしまい、 +`implode()` に渡すところでエラーになる。 +それを防ぐために `$r ?? []` を使っている。 + +もし 0 が渡されたケースを無視するなら、これが不要になるので 4 バイト縮む。 + +{#put-text-outside-php-tag} +## PHP タグの外に文字列を置く + +PHP では、`<?php` `?>` で囲われた部分の外側にある文字列は、そのまま出力される。 +今回のケースでは、先頭と末尾に必ず `[` と `]` を出力するので、そのまま書いてやればよい。 + +{#outro} +# おわりに + +最後になりましたが、 [めもりー](https://twitter.com/m3m0r7) さん、楽しい問題をありがとうございました。 diff --git a/services/blog/content/posts/2022-08-31/support-for-communty-is-employee-benefits.dj b/services/blog/content/posts/2022-08-31/support-for-communty-is-employee-benefits.dj new file mode 100644 index 00000000..1ba5891b --- /dev/null +++ b/services/blog/content/posts/2022-08-31/support-for-communty-is-employee-benefits.dj @@ -0,0 +1,52 @@ +--- +[article] +uuid = "cd16debe-8077-4edf-aec0-b1d45955a0e2" +title = "弊社の PHP Foundation への寄付に寄せて" +description = "先日、私の勤めるデジタルサーカス株式会社が、PHP Foundation へ寄付をおこないました。本件を社内でしつこく推進した1人として、推進の理由等を書き残しておきます。" +tags = [] + +[[article.revisions]] +date = "2022-08-31" +remark = "公開" +--- +{#intro} +# はじめに + +*注: これは私個人の意見であり、所属する組織を代表するものではありません。* + +先日、私の勤める [デジタルサーカス株式会社](https://www.dgcircus.com/) が +[PHP Foundation](https://opencollective.com/phpfoundation) へ $2,000 の寄付をおこないました。 + +記事: https://www.dgcircus.com/news/581 + +本件を社内でしつこく推進した1人として、推進の理由等を書き残しておきます。 + +{#why} +# なぜ? + +組織としての寄付理由は前掲した記事に譲るとして、ここでは、私が社内でこの件を推進した理由について書くことにします。 + +当時の考えを端的にまとめた社内チャットの投稿があったので、それを引用します: + +> 結局これを通したい (私の中での) 最大の理由が、「自分の勤める会社が、これをやる会社であってほしい」というのがあり、 +> ↑にしても、感情ベースの理由しか出せていないというのが説得力に欠けている理由なのだと思いますが、 +> 寄付の報告が流れてきたり、OSS のフリーライドの話が流れてきたりするたびに、自尊心が毀損される、というか +> (これは大袈裟すぎる表現で、実際にはそこまで明確に傷ついているわけではありませんが)。 +> +> 追記: 「肩身が狭くなる」というのがより適切でした。 + +※文中の「↑にしても」は、ここに載せていない別の投稿を指しています。 + +OSS を金銭的に支援したり、技術カンファレンスへ協賛したり +(あるいは [CTO](https://twitter.com/tomzoh) がカンファレンスを年2で主催したり: +[iOSDC](https://iosdc.jp) [PHPerKaigi](https://phperkaigi.jp) ) +といった行為は、コミュニティへの貢献であると同時に、社員に対する精神的福利厚生でもあると言えるでしょう (知らんけど)。 +これらは、技術や技術者を大切にする組織である、ということの、対外的にも対内的にも強力なメッセージなのです。 + +以上が、私が社内で寄付の件を進めた (かなり私的な) 理由です。 + +{#outro} +# おわりに + +最終的に社としての寄付まで漕ぎ着けられたのは、もちろん私の力ではなく役員の方々の決定によるものです。 +この場を借りて感謝申し上げます。 diff --git a/services/blog/content/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line.dj b/services/blog/content/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line.dj new file mode 100644 index 00000000..c23341dc --- /dev/null +++ b/services/blog/content/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line.dj @@ -0,0 +1,617 @@ +--- +[article] +uuid = "42f0b29b-1e44-4dbe-9864-69abe3bb1e6e" +title = "【PHP】 fizzbuzz を書く。1行あたり2文字で。" +description = "PHP で fizzbuzz を書いた。ただし、1行あたりに使える文字数は2文字まで。" +tags = [ + "php", +] + +[[article.revisions]] +date = "2022-09-28" +remark = "公開" + +[[article.revisions]] +date = "2022-09-29" +remark = "小さな文言の修正・変更" +--- +{#intro} +# 記事の構成について + +この記事は、普通の fizzbuzz を徐々に変形して最終形にしていく、という構成で書かれている。 +最終形を見てどのような仕組みで動いているのか解読してから解説を読みたい、というかたがいれば、 +[このページ](https://gist.github.com/nsfisis/04c227d5a419867472a0b23a83ad2919#file-fizzbuzz-php-2-letters-per-line-and-supports-php-8-x-without-warnings) +にソースコードがあるので、そちらを先に見てほしい。 + +{#regulations} +# レギュレーション + +PHP で、次のような制約の下に fizzbuzz を書いた。 + +* 1行あたりの文字数は2文字までに収めること (ただし `<?php` タグは除く) + + * 厳密な定義: `<?php` タグ以降のソースコードが、2 byte ごとにラインフィード (LF) で区切られること + +* スペースやタブを使用しないこと +* ループのアンロールをしないこと + + * 100 回ループの代わりに 100 回コードをコピペ、というのは禁止 + +* PHP 7.4〜8.1 で動作すること +* 実行時に Notice や Warning が出ないこと +* 標準的なインストール構成の PHP で実現できること (デフォルトで有効になっていない拡張等を使わないこと) + +備考: PHP には `short_open_tag` というオプションがあり、 +これを有効にするとファイル冒頭の `<?php` の代わりに `<?` +を使うことができ、文字どおり1行2文字で書ける。 +ただ、このオプションはデフォルト off になっている環境が多いようなので、今回は使わないことにした。 + +{#problems} +# 主な障害 + +1行あたりの文字数など、適当に改行を挟めばいいだけではないのか? + +特に、C言語でこのような試みをおこなったことがあるかたならそう思うだろう。事実、Cでのこの制約はほとんど無意味に等しい。 + +```c +#\ +i\ +n\ +c\ +l\ +u\ +d\ +e\ +<\ +s\ +t\ +d\ +i\ +o\ +.\ +h\ +>\ +/* +*/ +i\ +n\ +t\ +/* +*/ +m\ +a\ +i\ +n( +){ +f\ +o\ +r( +i\ +n\ +t\ +/* +*/ +i= +1; +i< +1\ +0\ +0; +i\ ++\ ++) +if +(i +%\ +15 +== +0) +p\ +r\ +i\ +n\ +t\ +f( +"\ +F\ +i\ +z\ +z\ +B\ +u\ +z\ +z\ +%\ +c\ +", +10 +); + +/* あとは同じように普通のプログラムを変形するだけなので省略 */ +``` + +バックスラッシュを使った行継続がトークンを区切らない、というのがポイントだ。 + +さて、PHP ではそもそもバックスラッシュを行継続に使うことができない。 +これにより、「3文字以上からなるトークンが一切使えない」という制約が課される。 +例えば、`echo` で出力することや、`for` でループすること、 +`new` でインスタンスを生成することができない。 +特に、出力は fizzbuzz をどんなアルゴリズムで実装しようとおこなわなければならないので、できないのは致命的である。 + +当然、名前が3文字以上ある関数も使えない。なお、標準 PHP の範囲内において、名前が 2文字以下の関数は以下のとおりである: + +* `_`: `gettext` のエイリアス +* `dl`: 拡張モジュールをロードする +* `pi`: 円周率を返す + +(環境によって多少は変わるかも) + +2文字の関数を定義しまくった拡張モジュールを用意しておいて `dl()` で読み込む行為は、レギュレーションで定めた + +> * 標準的なインストール構成の PHP で実現できること (デフォルトで有効になっていない拡張等を使わないこと) + +に反する (というより、「それだとおもしろくもなんともないので、このルールを足した」というのが正しい)。 + +また、2文字だと文字列がまともに書けないのも辛い。`''` だけで2文字使うので、 +「1文字の文字列リテラル」というものを書くことができない。PHP では文字列リテラル中に生の改行が書けるので + +```php +$a +=' +a' +;; +``` + +とすると `$a` は `"\na"` になるのだが、余計な改行が入ってしまう。 + +これらの障害をどのように乗り越えるのか、次節から見ていく。 + +{#commentary} +# 解説 + +{#normal-fizzbuzz} +## 普通の (?) fizzbuzz + +まずは普通に書くとしよう。 + +```php +<?php + +for ($i = 1; $i < 100; $i++) { + echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"; +} +``` + +素直に書いた fizzbuzz とは言い難いが、このくらいは普通だということにしておかないと、この先がやっていられないので許してほしい。 + +{#remove-keywords} +## `for` の排除 + +`for` は、3文字もある長いキーワードである。 +こんなものは使えない。`array_` 系の関数を使って、適当に置き換えるとしよう。 + +```php +<?php + +$s = range(1, 100); +array_walk( +$s, +fn($i) => +printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"), +); +``` + +`array_walk` や `range`、`printf` といった +`for` よりも長いトークンが現れてしまったが、これは次節で直すことにする。 +なお、`echo` は文 (statement) であり式 (expression) ではないので、式である `printf` に置き換えた。 + +{#shorten-function-invocation} +## 関数呼び出しの短縮 + +`range`、`array_walk`、`printf` は長すぎるのでどうにかせねばならない。 +ここで、PHP の可変関数を使う。可変関数とは、関数名が文字列として入った変数を経由して、関数を呼び出す機能である。 + +```php +<?php + +$r = 'range'; +$w = 'array_walk'; +$p = 'printf'; + +$s = $r(1, 100); +$w( +$s, +fn($i) => +$p((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"), +); +``` + +これで関数を呼び出している所は短くなった。 +では、`$r` や `$w` や `$p`、 +また `'Fizz'` や `'Buzz'` はどうやって 1 行 2 文字に収めるのか。 +次のテクニックへ移ろう。 + +{#incompatible-solution} +## 余談: PHP 8.x で動作しなくてもいいなら + +今回使ったテクニックを説明する前に、余談として、文字列リテラルの短縮法として今回採用しなかったものを紹介する。 + +> * PHP 7.4〜8.1 で動作すること + +というルールがない場合、「未定義の定数が評価された場合、その定数の名前が値になる」という PHP 7.x までの仕様が利用できる。 +例えば、 `Fizz` という文字列が欲しければ、次のようにする。 + +```php +$f +=F +.i +.z +.z +;; +``` + +こうして簡単に文字列を作れる。 +なお、この仕様は 7.x 時点でも警告を受けるので、`@` 演算子を使って抑制してやるとよい。 + +```php +$f +=@ +F. +@i +.# +@z +.# +@z +;; +``` + +むしろ、このことがわかっていたからこそ PHP 8.x での動作を要件に課したところがある。 + +{#shorten-string-literals} +## 文字列リテラルの短縮 + +実際に使った手法の説明に移る。 + +ずばり、文字列同士のビット演算を使う。 +PHP では、文字列同士でビット演算 (`&`、`|`、`^`) をした場合、 +文字列の各バイトごとに指定したビット演算がなされ、それを結合したものが演算結果となる。 + +```php +$a = "12345"; +$b = "world"; + +// $a ^ $b は次のコードと同じ +$result = ''; +for ($i = 0; $i < min(strlen($a), strlen($b)); $i++) { +$result .= $a[$i] ^ $b[$i]; +} + +echo $result; +// => F]AXQ +``` + +これを踏まえ、次のコードを見てみよう。 + +```php +$x = "x\nOm\n"; +$y = "\nk!\no"; +$r = $x ^ $y; +echo "$r\n"; +``` + +実行すると、`range` が表示される。 +さて、PHP では文字列リテラル中に生の改行を直接書いてもよいのだった (「主な障害」の節を参照のこと)。 +書きかえてみよう。 + +```php +$x +='x +Om +'; +$y +=' +k! +o' +; + +$r = $x ^ $y; +echo "$r\n"; +``` + +さらに `#` を使って適当に調整すると、次のようになる。 + +```php +$x +=# +'x +Om +'; +$y +=' +k! +o' +;# +$r +=# +$x +^# +$y +;# + +echo "$r\n"; +``` + +1行あたり2文字で、`range` という文字列を生成することに成功した。 +他の必要な文字列にも、同様の処理をほどこす。 + +備考: `Buzz` 中にある小文字の `u` は、このロジックだと non-printable な文字になってしまう。 +ここまでのテクニックを駆使すれば回避するのはそう難しくないので、考えてみてほしい。 + +{#stretched-fizzbuzz} +# 完成系 + +完成したものがこちら。 + +```php +<?php + +$x +=# +'i +S' +;; +$y +=' +b! +'; +$c +=# +$x +^# +$y +;# +$x +=# +'x +Om +'; +$y +=' +k! +o' +;# +$r +=# +$x +^# +$y +;# +$x +=# +'k +Sk +~} +Ma +'; +$y +=' +x! +s! +k! +'; +$w +=# +$x +^# +$y +;# +$x +=# +'z +Hd +G' +;# +$y +=' +x! +~! +'; +$p +=# +$x +^# +$y +;# +$x +=# +'L +[p +'; +$y +=' +c! +'; +$f +=# +$x +^# +$y +;# +$x +=# +'H +[p +'; +$y +=' +_! +'; +$b +=# +$x +^# +$y +;# +$b +[1 +]= +$c +(# +13 +*9 +); +$s +=# +$r +(1 +,( +10 +** +2) +); +$w +(# +$s +,# +fn +(# +$i +)# +=> +$p +(( +(# +$i +%3 +?# +'' +:# +$f +). +(# +$i +%5 +?# +'' +:# +$b +)? +:# +$i +)# +.' +') +); +``` + +{#outro} +# 感想など + +PHP は、スクリプト言語の中だとシンタックスシュガーが少ない (体感)。 +この挑戦は不可能に思われたが、PHP マニュアルとにらめっこしていたらなんとかなった。 + +みんなもプログラムを細長くしよう。 + +{#alternative-solution} +# 余談2: 別解 + +PHP では、バッククォートを使ってシェルを呼び出せる。 +これは `shell_exec` 関数と等価である。 +さて、PHP ではバックスラッシュによる行継続が使えないと書いたが、シェルでは使える +(当然だが、呼び出されるシェルに依存する。Bash なら大丈夫だろう。知らんけど)。 + +```php +<?php + +printf(` +e\ +c\ +h\ +o\ +\ +1\ +2\ +3\ +`); +``` + +なお、ここでは簡単のため出力に `printf` をそのまま使っているが、 +実際には `printf` という文字列を合成して可変関数で呼び出す。 + +ただし、これでは + +> * スペースやタブを使用しないこと + +に違反してしまう。スペースが使えないと引数とコマンドを区切れない。これは困った。 + +もうこれ以上は不可能だと思っていたのだが、この記事の執筆中に解決する方法を思いついたので載せておく。 + +```php +<?php + +$c = 'chr'; + +${ +'_ +'} +=# +$c +(# +32 +). +$c +(# +92 +); + +printf(` +e\ +c\ +h\ +o\ +${ +'_ +'} +1\ +2\ +3\ +`); +``` + +先程と同じく、`chr` や `printf` を生成する部分は長くなるので省いた。 + +``` +${ +'_ +'} +``` + +は変数で、中にはスペースとエスケープが入っている (`chr(32) . chr(92)`)。 +シェルに渡されている文字列は次のようになる。 + +``` +e\ +c\ +h\ +o\ +\ +1\ +2\ +3\ +``` + +これは、前掲したコマンドと同じだ。 +かくして、スペースを陽に書かずにシェルをおおよそ自由に扱えるようになった。 +Fizzbuzz のワンライナーくらいすぐ書けるだろうから、あとはなんとかなるだろう (試してないけど)。 + +ということでこれは別解ということにしておく。 + +ちなみに、PHP 8.2 からは、この記法で Warning が出るようになるようだ。 + +``` +${ +'_ +'} +``` + +最新版で警告が出るというのも美しくないので、私としては本編の解法を推す。 diff --git a/services/blog/content/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1.dj b/services/blog/content/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1.dj new file mode 100644 index 00000000..8567c711 --- /dev/null +++ b/services/blog/content/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1.dj @@ -0,0 +1,154 @@ +--- +[article] +uuid = "46e0d5db-b17e-464c-a723-8c3e01af7d1d" +title = "PHPerKaigi 2023: ボツになったトークン問題 その 1" +description = "来年の PHPerKaigi 2023 でデジタルサーカス株式会社から出題予定のトークン問題のうち、ボツになった問題を公開する (その 1)。" +tags = [ + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2022-10-23" +remark = "公開" +--- +{#intro} +# はじめに + +2023 年 3 月 23 日から 25 日にかけて開催予定 (記事執筆時点) の、 +[PHPerKaigi 2023](https://phperkaigi.jp/2023/) において、 +昨年と同様に、弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) から、 +トークン問題を出題予定である。 + +昨年のトークン問題の記事はこちら: [PHPerKaigi 2022 トークン問題の解説](/posts/2022-04-09/phperkaigi-2022-tokens) + +すでに 2023 年用の問題は作成済みであるが、その制作過程の中でいくつかボツ問ができた。 +せっかくなので、PHPerKaigi 開催を待つ間に紹介しようと思う。 + +10 月から 2 月まで、毎月 1 記事ずつ公開していく予定 (忘れていなければ)。 + +{#quiz} +# 問題 + +注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。 + +```php +<?php + +$π = $argv[1] ?? null; +if ($π === null) { + exit('No input.'); +} +$π = trim($π); +if (!is_numeric($π)) { + exit('Invalid input.'); +} + +$s = implode(array_map(chr(...), str_split($π, 2))); + +preg_match('/(\x23.+?) /', $s, $m); +$t = $m[1] ?? ''; + +if (md5($t) === '056e831a4146bf123e8ea16613303d2e') { + echo "Token: {$t}\n"; +} else { + echo "Failed.\n"; +} +``` + +{#how-to-obtain-token} +# トークン入手方法 + +ソースを見るとわかるとおり、`$argv[1]` を参照している。 +それを `$π` なる変数に代入しているので、円周率を渡してみる。 + +```shell-session +$ php Q.php 3.14 +Failed. +``` + +失敗してしまった。精度を上げてみる。 + +```shell-session +$ php Q.php 3.1415 +Failed. +``` + +だめだった。これを成功するまで繰り返す。 + +最初にトークンが得られるのは、小数点以下 16 桁目まで入力したときで、こうなる。 + +```shell-session +$ php Q.php 3.1415926535897932 +Token: #YO +``` + +めでたくトークン「`#YO`」が手に入った。 + +{#commentary} +# 解説 + +短いので頭から追っていく。 + +```php +$π = $argv[1] ?? null; +if ($π === null) { + exit('No input.'); +} +$π = trim($π); +if (!is_numeric($π)) { + exit('Invalid input.'); +} +``` + +入力のバリデーション部分。数値のみ受け付ける。 + +```php +$s = implode(array_map(chr(...), str_split($π, 2))); +``` + +`$π` を 2 文字ごとに区切り (`str_split`)、 +数値を ASCII コードと見做して文字に変換 (`chr`) して結合 (`implode`) している。 + +例えば、`$π` が `'656667'` だったとすると、 +`65`、`66`、`67` に対応した +`'A'`、`'B'`、`'C'` へと変換され、`'ABC'` になる。 + +```php +$π = '656667'; +$s = implode(array_map(chr(...), str_split($π, 2))); +echo $s; +// => ABC +``` + +```php +preg_match('/(\x23.+?) /', $s, $m); +$t = $m[1] ?? ''; +``` + +正規表現でマッチングしている。`\x23` は `#` と同じであることに留意すると、 +この正規表現は「`#` から始まる 2 以上の長さ (含 `#`) の文字列で、 +最初に現れるスペースまで」にマッチする。つまりこれは、PHPerKaigi におけるトークンである。 + +なお、`#` を直接書いていないのは、`/#.+?) /` と書くと、 +`#.+?)` という意図せぬトークンが登録されてしまうからである。 + +```php +if (md5($t) === '056e831a4146bf123e8ea16613303d2e') { + echo "Token: {$t}\n"; +} else { + echo "Failed.\n"; +} +``` + +最後にトークンのハッシュ値を見て、想定解かどうかを確認する。 + +{#outro} +# おわりに + +円周率を何桁も計算して ASCII コード経由で文字列化すれば、トークンっぽいものがどこかで出てくるのではないか、と考えて生まれた作品。 + +最初は真面目に円周率の計算プログラムを組んでいたのだが、いざ動かしてみるとやけに浅いところにあったので驚いた +(ちなみに、それでも `M_PI` や `pi()` では精度が足りない)。 +見つけたときは狂喜したものの、冷静になってみると大して面白くなかったのでボツになった。 +むしろ、100 万桁目くらいに埋まっていてくれたほうがよかったかもしれない。 diff --git a/services/blog/content/posts/2022-10-28/setup-server-for-this-site.dj b/services/blog/content/posts/2022-10-28/setup-server-for-this-site.dj new file mode 100644 index 00000000..6fed329d --- /dev/null +++ b/services/blog/content/posts/2022-10-28/setup-server-for-this-site.dj @@ -0,0 +1,264 @@ +--- +[article] +uuid = "673cb872-af2d-41a3-9fb0-60f1afcedb0d" +title = "【備忘録】 このサイト用の VPS をセットアップしたときのメモ" +description = "GitHub Pages でホストしていたこのサイトを VPS へ移行したので、そのときにやったことのメモ。99 % 自分用。" +tags = [ + "note-to-self", +] + +[[article.revisions]] +date = "2022-10-28" +remark = "公開" + +[[article.revisions]] +date = "2023-08-30" +remark = "ssh_config に IdentitiesOnly yes を追加" +--- +{#intro} +# はじめに + +これまでこの blog は GitHub Pages でホストしていたのだが、先日 VPS に移行した。 +そのときにおこなったサーバのセットアップ作業を書き残しておく。 +99 % 自分用の備忘録。別のベンダに移したりしたくなったら見に来る。 + +未来の自分へ: 特に自動化してないので、せいぜい苦しんでくれ。 + +{#vps} +# VPS + +[さくらの VPS](https://vps.sakura.ad.jp/) の 2 GB プラン。 +そこまで真面目に選定していないので、困ったら移動するかも。 + +{#preparation} +# 事前準備 + +{#hostname} +## サーバのホスト名を決める + +モチベーションが上がるという効能がある。今回は藤原定家から取って `teika` にした。 +たいていいつも源氏物語の帖か小倉百人一首の歌人から選んでいる。 + +{#ssh-key} +## SSH の鍵生成 + +ローカルマシンで鍵を生成する。 + +```shell-session +$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/teika.key +$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github2teika.key +``` + +`teika.key` はローカルからサーバへの接続用、`github2teika.key` は、 +GitHub Actions からサーバへのデプロイ用。 + +{#ssh-config} +## SSH の設定 + +`.ssh/config` に設定しておく。 + +```ssh_config +Host teika + HostName ********** + User ********** + Port ********** + IdentityFile ~/.ssh/teika.key + IdentitiesOnly yes +``` + +{#basic-setup} +# 基本のセットアップ + +{#login} +## SSH 接続 + +VPS 契約時に設定した管理者ユーザとパスワードを使ってログインする。 + +{#user} +## ユーザを作成する + +管理者ユーザで作業すると危ないので、メインで使うユーザを作成する。 +`sudo` グループに追加して `sudo` できるようにし、`su` で切り替え。 + +```shell-session +$ sudo adduser ********** +$ sudo adduser ********** sudo +$ su ********** +$ cd +``` + +{#hostname} +## ホスト名を変える + +```shell-session +$ sudo hostname teika +``` + +{#public-key} +## 公開鍵を置く + +```shell-session +$ mkdir ~/.ssh +$ chmod 700 ~/.ssh +$ vi ~/.ssh/authorized_keys +``` + +`authorized_keys` には、ローカルで生成した `~/.ssh/teika.key.pub` と +`~/.ssh/github2teika.key.pub` の内容をコピーする。 + +{#ssh-config} +## SSH の設定 + +SSH の設定を変更し、少しでも安全にしておく。 + +```shell-session +$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak +$ sudo vi /etc/ssh/sshd_config +``` + +* `Port` を変更 +* `PermitRootLogin` を `no` に +* `PasswordAuthentication` を `no` に + +そして設定を反映。 + +```shell-session +$ sudo systemctl restart sshd +$ sudo systemctl status sshd +``` + +{#ssh-connect} +## SSH で接続確認 + +今の SSH セッションは閉じずに、ターミナルを別途開いて疎通確認する。 +セッションを閉じてしまうと、SSH の設定に不備があった場合に締め出しをくらう。 + +```shell-session +$ ssh teika +``` + +{#close-ports} +## ポートの遮断 + +デフォルトの 22 番を閉じ、設定したポートだけ空ける。 + +```shell-session +$ sudo ufw deny ssh +$ sudo ufw allow ******* +$ sudo ufw enable +$ sudo ufw reload +$ sudo ufw status +``` + +ここでもう一度 SSH の接続確認を挟む。 + +{#ssh-key-for-github} +## GitHub 用の SSH 鍵 + +GitHub に置いてある private リポジトリをサーバから clone したいので、SSH 鍵を生成して置いておく。 + +```shell-session +$ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github.key +$ cat ~/.ssh/github.key.pub +``` + +[GitHub の設定画面](https://github.com/settings/ssh) から、この公開鍵を追加する。 + +```shell-session +$ vi ~/.ssh/config +``` + +設定はこう。 + +```ssh_config +Host github.com + HostName github.com + User git + Port 22 + IdentityFile ~/.ssh/github.key + IdentitiesOnly yes +``` + +最後に接続できるか確認しておく。 + +```shell-session +$ ssh -T github.com +``` + +{#upgrade-packages} +## パッケージの更新 + +```shell-session +$ sudo apt update +$ sudo apt upgrade +$ sudo apt update +$ sudo apt upgrade +$ sudo apt autoremove +``` + +{#site-hosting-setup} +# サイトホスティング用のセットアップ + +{#dns} +## DNS に IP アドレスを登録する + +このサーバは固定の IP アドレスがあるので、`A` レコードに直接入れるだけで済んだ。 + +{#install-softwares} +## 使うソフトウェアのインストール + +```shell-session +$ sudo apt install docker docker-compose git make +``` + +{#docker} +## メインユーザが Docker を使えるように + +```shell-session +$ sudo adduser ********** docker +``` + +{#open-http-ports} +## HTTP/HTTPS を通す + +80 番と 443 番を空ける。 + +```shell-session +$ sudo ufw allow 80/tcp +$ sudo ufw allow 443/tcp +$ sudo ufw reload +$ sudo ufw status +``` + +{#clone-repositories} +## リポジトリのクローン + +```shell-session +$ cd +$ git clone git@github.com:nsfisis/nsfisis.dev.git +$ cd nsfisis.dev +$ git submodule update --init +``` + +{#certbot} +## certbot で証明書取得 + +```shell-session +$ docker-compose up -d acme-challenge +$ make setup +``` + +{#run-server} +## サーバを稼動させる + +```shell-session +$ make serve +``` + +{#outro} +# 感想 + +(業務でなく) 個人だと数年ぶりのサーバセットアップで、これだけでも割と時間を食ってしまった。 +とはいえ式年遷宮は楽しいので、これからも定期的にやっていきたい。 +コンテナデプロイにしたい気持ちもあるのだが、色々実験したい関係上、本物のサーバも欲しくはある。 +次の式年遷宮では、手順の一部だけでも自動化したいところ。 diff --git a/services/blog/content/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2.dj b/services/blog/content/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2.dj new file mode 100644 index 00000000..bd752c2e --- /dev/null +++ b/services/blog/content/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2.dj @@ -0,0 +1,139 @@ +--- +[article] +uuid = "10fe9c47-7029-4874-82bd-b4dc50e07809" +title = "PHPerKaigi 2023: ボツになったトークン問題 その 2" +description = "来年の PHPerKaigi 2023 でデジタルサーカス株式会社から出題予定のトークン問題のうち、ボツになった問題を公開する (その 2)。" +tags = [ + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2022-11-19" +remark = "公開" +--- +{#intro} +# はじめに + +2023 年 3 月 23 日から 25 日にかけて開催予定 (記事執筆時点) の [PHPerKaigi 2023](https://phperkaigi.jp/2023/) において、 +昨年と同様に、弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) からトークン問題を出題予定である。 + +昨年のトークン問題の記事はこちら: [PHPerKaigi 2022 トークン問題の解説](/posts/2022-04-09/phperkaigi-2022-tokens/) + +すでに 2023 年用の問題は作成済みであるが、その制作過程の中でいくつかボツ問ができた。せっかくなので、PHPerKaigi 開催を待つ間に紹介しようと思う。 + +10 月から 2 月まで、毎月 1 記事ずつ公開していく予定 (忘れていなければ)。 + +その 1 はこちら: [PHPerKaigi 2023: ボツになったトークン問題 その 1](/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/) + +{#quiz} +# 問題 + +注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。 + +```php +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +``` + +"And Then There Were None" (そして誰もいなくなった) と名付けた作品。変則 quine (自分自身と同じソースコードを出力するプログラム) になっている。 + +{#how-to-obtain-token} +# トークン入手方法 + +実行してみると、次のような出力が得られる。 + +```php +# +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +``` + +1 行目を除き、先ほどのコードとほぼ同じものが出てきた。もう一度実行してみる。 + +```php +# +W +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s='<?php printf((isset($s)?fn($s)=>trim($s,""):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +``` + +今度は 2 行目が書き換えられた。すべての行が変化するまで繰り返すと次のようになる。 + +```php +# +W +E +L +O +V +E +P +H +P +``` + +トークン「#WELOVEPHP」が手に入った。 + +{#commentary} +# 解説 + +一見すると同じ行が 10 行並んでいるだけなのにも関わらず、なぜそれぞれの行で出力が変わるのか。ソースコードをコピーして、適当なエディタに貼り付けるとわかりやすい。 + +Vim で開くと次のようになる (1 行目を抜粋)。 + +```php +<?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>')."\n","\x27$s\x27");?> +``` + +`<200b>` と表示されているのは、Unicode の U+200b で、ゼロ幅スペースである。 + +::: note +エディタによっては、ゼロ幅スペースが見えないことがある。VSCode ではブラウザと同様に不可視だった。 +::: + +文字列リテラルの中にゼロ幅スペースを仕込むことで、見た目を変えずに情報をエンコードすることが可能となる。 + +続いて、トークンへの変換ロジックを解析する。注目すべきはこの部分だ。以下、ゼロ幅スペースは Vim での表示に合わせて `<200b>` と記載する。 + +```php +fn($s)=>chr(strlen($s)/3) +``` + +PHP の `strlen()` は文字列のバイト数を返す。1 行目の `$s` は以下の内容となっており、 + +```php +$s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>' +``` + +このソースコードは UTF-8 で書かれているので、105 バイトになる。それを 3 で割ると 35 となり、これは `#` の ASCII コードと一致する。他の行も、同様にしてゼロ幅スペースを詰めることで文字列長を調整し、トークンをエンコードしている。 + +デコード部以外の部分は、quine のための記述である。 + +{#outro} +# おわりに + +[CVE-2021-42574](https://blog.rust-lang.org/2021/11/01/cve-2021-42574.html) に着想を得た作品。この脆弱性は、Unicode の制御文字である left-to-right mark と right-to-left mark を利用し、ソースコードの実際の内容を欺く、というもの。簡単のためゼロ幅スペースを用いることとし、ついでに quine にもするとこうなった。 + +ボツになった理由は、ゼロ幅スペースを表示してくるエディタが想像以上に多かったため。「同じ行が並んでいるだけなのに出力が異なる」というアイデアの根幹を崩されてしまうので、この問題は不採用となった。 diff --git a/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj b/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj new file mode 100644 index 00000000..9cbb15be --- /dev/null +++ b/services/blog/content/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3.dj @@ -0,0 +1,289 @@ +--- +[article] +uuid = "89722cfb-7f4b-4e96-80bc-e0096e5eeef6" +title = "PHPerKaigi 2023: ボツになったトークン問題 その 3" +description = "来年の PHPerKaigi 2023 でデジタルサーカス株式会社から出題予定のトークン問題のうち、ボツになった問題を公開する (その 3)。" +tags = [ + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2023-01-10" +remark = "公開" +--- +{#intro} +# はじめに + +2023 年 3 月 23 日から 25 日にかけて開催予定 (記事執筆時点) の [PHPerKaigi 2023](https://phperkaigi.jp/2023/) において、 +昨年と同様に、弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) からトークン問題を出題予定である。 + +昨年のトークン問題の記事はこちら: [PHPerKaigi 2022 トークン問題の解説](/posts/2022-04-09/phperkaigi-2022-tokens/) + +すでに 2023 年用の問題は作成済みであるが、その制作過程の中でいくつかボツ問ができた。 +せっかくなので、PHPerKaigi 開催を待つ間に紹介しようと思う。 + +10 月から 2 月まで、毎月 1 記事ずつ公開していく予定 (忘れていなければ → 忘れていたので 12 月公開予定だった記事を今書いている)。 + +* その 1 はこちら: [PHPerKaigi 2023: ボツになったトークン問題 その 1](/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/) +* その 2 はこちら: [PHPerKaigi 2023: ボツになったトークン問題 その 2](/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/) + +{#quiz} +# 問題 + +注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。 + +```php +<?php +try { + f(g() / __LINE__); +} catch (Throwable $e) { + while ($e = $e->getPrevious()) printf('%c', $e->getLine() + 23); + echo "\n"; +} +function f(int $i) { + if ($i < 0) f(); + try { + match ($i) { + 0 => 0 / 0, + + + + 15, 36 => 0 / 0, + 14 => 0 / 0, + 37 => 0 / 0, + + + + + + + + + + + 6 => 0 / 0, + + 5 => 0 / 0, + + 22 => 0 / 0, + + + + + 34, 35 => 0 / 0, + + + + + + + + + 25 => 0 / 0, + 17, 21 => 0 / 0, + + 24, 32 => 0 / 0, + + + + + + + + 33 => 0 / 0, + + 16 => 0 / 0, + + + 18 => 0 / 0, + + + + + + + + + 7 => 0 / 0, + + 2 => 0 / 0, + 1, 20 => 0 / 0, + 10, 28 => 0 / 0, + 8, 12, 26 => 0 / 0, + 4, 9, 13 => 0 / 0, + + + + + + 31 => 0 / 0, + + 29 => 0 / 0, + + 11 => 0 / 0, + + + + 3, 19, 23 => 0 / 0, + + + 27 => 0 / 0, + + 30 => 0 / 0, + }; + } finally { + f($i - 1); + } +} + + + + + + + +function g() { + return __LINE__; +} +``` + +"Catchline" と名付けた作品。実行するとトークン `#base64_decode('SGVsbG8sIFdvcmxkIQ==')` が得られる。 + +トークンは PHP の式になっていて、評価すると `Hello, World!` という文字列になる。PHPer チャレンジのトークンには空白を含められないという制約があるが、こういった形でトークンにすれば回避できる。 + +{#commentary} +# 解説 + +{#summary} +## 概要 + +例外が発生した行数にデータをエンコードし、それを `catch` で捕まえて表示している。 + +{#chain-of-exceptions} +## 例外オブジェクトの連鎖 + +[`Exception`](https://www.php.net/class.Exception) や [`Error`](https://www.php.net/class.Error) には `$previous` というプロパティがあり、コンストラクタの第3引数から渡すことができる。主に 2つの用法がある: + +* エラーを処理している途中に起こった別のエラーに、元のエラー情報を含める +* 内部エラーをラップして作られたエラーに、内部エラーの情報を含める + +このうち 1つ目のケースは、 `finally` 節の中でエラーを投げると PHP 処理系が勝手に `$previous` を設定してくれる。 + +```php +<?php + +try { + try { + throw new Exception("Error 1"); + } finally { + throw new Exception("Error 2"); + } +} catch (Exception $e) { + echo $e->getMessage() . PHP_EOL; + // => Error 2 + echo $e->getPrevious()->getMessage() . PHP_EOL; + // => Error 1 +} +``` + +この知識を元に、トークンの出力部を解析してみる。 + +{#output} +## 出力部の解析 + +出力部をコメントや改行を追加して再掲する: + +```php +<?php +try { + f(g() / __LINE__); +} catch (Throwable $e) { + while ($e = $e->getPrevious()) { + printf('%c', $e->getLine() + 23); + } + echo "\n"; +} +``` + +出力をおこなう `catch` 節を見てみると、 `Throwable::getPrevious()` を呼び出してエラーチェインを辿り、 `Throwable::getLine()` でエラーが発生した行数を取得している。その行数に `23` なるマジックナンバーを足し、フォーマット指定子 `%c` で出力している。 + +フォーマット指定子 `%c` は、整数を ASCII コード[^ras-syndrome] と見做して印字する。トークン `#base64_decode('SGVsbG8sIFdvcmxkIQ==')` の `b` であれば、ASCII コード `98` なので、75 行目で発生したエラー、 + +```php +1, 20 => 0 / 0, +``` + +によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。 + +それでは、エラーチェインを作る箇所、関数 `f()` を見ていく。 + +[^ras-syndrome]: RAS syndrome + +{#data-construction} +## データ構成部の解析 + +`f()` の定義を再掲する (エラーオブジェクトの行数を利用しているので、一部分だけ抜き出すと値が変わることに注意): + +```php +function f(int $i) { + if ($i < 0) f(); + try { + match ($i) { + 0 => 0 / 0, // 12 行目 + + + + 15, 36 => 0 / 0, + 14 => 0 / 0, + 37 => 0 / 0, + + // (略) + + 30 => 0 / 0, // 97 行目 + }; + } finally { + f($i - 1); + } +} +``` + +前述のように、 `finally` 節でエラーを投げると PHP 処理系が `$previous` を設定する。ここでは、エラーを繋げるために `f()` を再帰呼び出ししている。最初に `f()` を呼び出している箇所を確認すると、 + +```php +<?php +try { + f(g() / __LINE__); // 3 行目 +``` + +```php +function g() { + return __LINE__; // 111 行目 +} +``` + +`f()` には `111 / 3` で `37` が渡されることがわかる。そこから 1 ずつ減らして再帰呼び出ししていき、0 より小さくなったら `f()` を引数なしで呼び出す。引数の数が足りないと呼び出しに失敗するので、再帰はここで止まる。 + +エラーチェインは、最後に発生したエラーを先頭とした単方向連結リストになっているので、順に + +1. `f()` の引数が足りないことによる呼び出し失敗 +1. `f(0)` の呼び出しで発生したゼロ除算 +1. `f(1)` の呼び出しで発生したゼロ除算 +1. … +1. `f(37)` の呼び出しで発生したゼロ除算 + +となっている。出力の際は `catch` したエラーの `getPrevious()` から処理を始めるので、1 番目の `f()` によるエラーは無視され、 `f(0)` によるエラー、 `f(1)` によるエラー、 `f(2)` によるエラー、と出力が進む。 + +`f()` に `0` を渡したときは 12 行目にある `match` の `0` でゼロ除算が起こるので、行数が 12 となったエラーが投げられる。出力部ではこれに 23 を足した数を ASCII コードとして表示しているのだった。 `12 + 23` は `35`、ASCII コードでは `#` である。これがトークンの 1文字目にあたる。 + +{#outro} +# おわりに + +「行数」というのはトークン文字列をデコードする対象として優れている。 + +* トークンの一部や全部が陽に現れない +* `__LINE__` で容易に取得できる + +しかし、こういった「変な」プログラムを何度も読んだり書いたりしていると、 `__LINE__` を使うのはあまりにありきたりで退屈になる。では、他に行数を取得する手段はないか。こうして `Throwable` を思いつき、続けてエラーオブジェクトには `$previous` があることを思い出した。 + +今回エラーを投げるのにゼロ除算を用いたのは、それがエラーを投げる最も短いコードだと考えたからである。もし 3バイト未満で `Throwable` なオブジェクトを投げる手段をご存じのかたがいらっしゃれば、ぜひご教示いただきたい。……と締める予定だったのだが、`0/0` のところを存在しない定数にすれば、簡単に 1バイトを達成できた。ゼロ除算している箇所はちょうど 26 箇所あるので、アルファベットにでもしておけば意味ありげで良かったかもしれない。 diff --git a/services/blog/content/posts/2023-03-10/rewrite-this-blog-generator.dj b/services/blog/content/posts/2023-03-10/rewrite-this-blog-generator.dj new file mode 100644 index 00000000..a4ccf87e --- /dev/null +++ b/services/blog/content/posts/2023-03-10/rewrite-this-blog-generator.dj @@ -0,0 +1,73 @@ +--- +[article] +uuid = "12512894-05d8-42c6-950e-8f5d60f984d8" +title = "このブログのジェネレータを書き直した" +description = "このブログのジェネレータを書き直したので、やったことを書き記しておく。" +tags = [] + +[[article.revisions]] +date = "2023-03-10" +remark = "公開" +--- +{#intro} +# はじめに + +このブログを構築するシステムを書き直したのは 2度目である。 +元々立ち上げた当初は、静的サイトジェネレータである [Hugo](https://gohugo.io/) を使っていた。 +それを [Asciidoctor](https://asciidoctor.org/) にいくつかのカスタムを加えた自前のジェネレータに移行したのが 2022年の11月ごろだ。 +そして今回、スクラッチから書いた [Deno](https://deno.land/) 製のジェネレータに移行した。 + +この記事では、移行の理由などを (主に将来の私へ向けて) 書き記しておく。 + +{#from-hugo-to-asciidoctor} +# Hugo から Asciidoctor へ + +最初に断っておくと、Hugo は大変に優れた静的サイトジェネレータである。移行の理由の大半は、自分でジェネレータを書きたかったからに他ならない。 +実のところ、この記事を執筆している現在、自作ジェネレータは Hugo よりも機能が劣っている。 +例えば、Hugo を使っていたころはサポートしていた RSS フィードの生成は、まだ実装できていない。 + +移行先のフォーマットとして AsciiDoc を選んだのは、Markdown よりも表現力に優れるからである。Markdown は広く使われている軽量マークアップ言語だが、以下のような欠点を持つ。 + +* CommonMark では機能が貧弱である (例: 脚注、`id` 属性の付与) +* 拡張記法に実装間で互換性がない +* メタデータ (公開日など) を埋め込む統一された方法がない + +AsciiDoc は Markdown に比べると普及していないが、上記の欠点は克服している。 + +* ブログを書くのに十分な表現力がある +* フォーマットを拡張するときの記法があらかじめ定められている +* メタデータを埋め込む統一された方法がある + +なお、Hugo は AsciiDoc もサポートしているのだが、AsciiDoc を使う場合 Asciidoctor を別途インストールする必要があり、それならば最初から Asciidoctor でよかろうと移行を決めた。 + +{#from-asciidoctor-to-my-own-generator} +# Asciidoctor から自前のジェネレータへ + +AsciiDoc は良いフォーマットだが、私には 1点不満があった。それは、高い表現力を担保するために記号が使い倒されており、エスケープが難しいという点だ (具体例を挙げたいのだが、何だったか覚えていない)。これは、多種多様な記号類を入力する必要のある技術ブログにとっては辛い問題である。この問題を解決するため、 + +* 表現力が高く、 +* 文法が厳密であり、 +* 簡単に実装できる + +フォーマットが求められた。これに合致したのが、XML をベースとする [DocBook](https://docbook.org/) (今回使っているのは、そのサブセットである [Simplified DocBook](https://tdg.docbook.org/tdg/sdocbook/5.1/) ) である。 + +実は、AsciiDoc と DocBook はおおよそ互換性がある。AsciiDoc で書かれた文書は (ほぼ) 情報ロスなしに DocBook へ変換でき、逆もまたしかりである。 +よって、DocBook には、AsciiDoc と同等の表現力がある。 + +XML の文法の厳密さについては、説明するまでもないだろう。また、単純な文法であることから実装が容易であり、事実上 Asciidoctor へロックインされる AsciiDoc とは異なり、さまざまな言語で多くのライブラリが存在する。 + +今回は、XML のパース自体も自分で書いている (これは何となく書きたかったからであり、合理的な理由があるわけではない。実装はサボりまくっているので XML のコメントが使えないといった制限がある)。 + +XML という機械処理しやすいフォーマットを選ぶことには、機械的な変換や検査といった処理がおこないやすくなるといった利点もある。 +欠点は軽量マークアップ言語と比べて冗長であることだが、書く際は補完などを用いるのでそれほど気にならない。 +結局のところ、技術ブログの執筆を律速するのは調査と文章の記述であり、マークアップの手段は執筆時間に大した影響を与えない。 + +{#outro} +# おわりに + +2度のリライトを経て、記事のフォーマットとサイトジェネレータを上から下まで掌握した。 +今後も改善のアイデアは多数あるので、じわじわと進めていきたいところだ。 + +最後にもう一度書くのだが、Hugo は大変に優れた静的サイトジェネレータである。 +無駄な拘りがなければこれを使うとよい。 +私は無駄に拘ったので、ブログの記事を書く時間を潰してブログシステムを作ってしまった。 diff --git a/services/blog/content/posts/2023-04-01/implementation-of-minimal-png-image-encoder.dj b/services/blog/content/posts/2023-04-01/implementation-of-minimal-png-image-encoder.dj new file mode 100644 index 00000000..55d1519b --- /dev/null +++ b/services/blog/content/posts/2023-04-01/implementation-of-minimal-png-image-encoder.dj @@ -0,0 +1,544 @@ +--- +[article] +uuid = "ed36e185-5bfa-42e1-8358-0b1da9b0a063" +title = "PNG 画像の最小構成エンコーダを実装する" +description = "PNG 画像として valid な範囲で最大限手抜きしたエンコーダを書く。" +tags = [] + +[[article.revisions]] +date = "2023-04-01" +remark = "公開" +--- +{#intro} +# はじめに + +この記事では、PNG 画像として valid な範囲で最大限手抜きしたエンコーダを書く。 +PNG 画像に対応したビューアであれば読み込めるが、圧縮効率については一切考えない。 +また、実装には Go 言語を使うが、Go の標準ライブラリにあるさまざまなアルゴリズム (PNG 画像に関係する範囲だと、zlib や CRC32、Adler-32 など) は使わない。 + +{#basic-structure-of-png} +# PNG ファイルの基本構造 + +PNG ファイルの基本構造は次のようになっている。 + +1. PNG signature +1. IHDR chunk +1. 任意個の chunk +1. IEND chunk + +Chunk には画像データを入れる IDAT chunk、パレットデータを入れる PLTE chunk、テキストデータを入れる tEXt chunk などがあるが、 +今回は最小構成ということで IDAT chunk (と IHDR chunk と IEND chunk) のみを用いる。 + +次節で、それぞれの具体的な構造を確認しつつ実装していく。 + +{#implement-png-encoder} +# PNG のエンコーダを実装する + +以下のソースコードをベースにする。 +今回 PNG のデコーダは扱わないので、読み込みには Go の標準ライブラリ `image/png` を用いる。 + +```go +package main + +import ( + "image" + _ "image/png" + "io" + "os" +) + +func main() { + inFile, err := os.Open("input.png") + if err != nil { + panic(err) + } + defer inFile.Close() + + img, _, err := image.Decode(inFile) + if err != nil { + panic(err) + } + + outFile, err := os.Create("output.png") + if err != nil { + panic(err) + } + defer outFile.Close() + + writePng(outFile, img) +} + +func writePng(w io.Writer, img image.Image) { + width := uint32(img.Bounds().Dx()) + height := uint32(img.Bounds().Dy()) + writeSignature(w) + writeChunkIhdr(w, width, height) + writeChunkIdat(w, width, height, img) + writeChunkIend(w) +} +``` + +以降は、`writeSignature` や `writeChunkIhdr` などを実装していく。 + +{#png-signature} +## PNG signature + +PNG signature は、PNG 画像の先頭に固定で付与されるバイト列で、8 バイトからなる。 + +1. 0x89 +1. 0x50 (ASCII コードで「P」) +1. 0x4E (ASCII コードで「N」) +1. 0x47 (ASCII コードで「G」) +1. 0x0D (ASCII コードで CR) +1. 0x0A (ASCII コードで LF) +1. 0x1A (ASCII コードで EOF) +1. 0x0A (ASCII コードで LF) + +CRLF や LF は、送信中に改行コードの変換が誤っておこなわれていないかどうかを検知するのに使われる。 + +`writeSignature` の実装はこちら: + +```go +import "encoding/binary" + +func writeSignature(w io.Writer) { + sig := [8]uint8{ + 0x89, + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // CR + 0x0A, // LF + 0x1A, // EOF (^Z) + 0x0A, // LF + } + binary.Write(w, binary.BigEndian, sig) +} +``` + +`encoding/binary` パッケージの `binary.Write` を使い、固定の 8 バイトを書き込む。 + +{#structure-of-chunk} +## Chunk の構造 + +IHDR chunk に進む前に、chunk 一般の構造を確認する。 + +1. Length: chunk data のバイト長 (符号なし 4 バイト整数) +1. Chunk type: chunk の種類を示す 4 バイトからなる名前 +1. Chunk data: 実際のデータ。0 バイトでもよい +1. CRC: chunk type と chunk data の CRC (符号なし 4 バイト整数) + +CRC (Cyclic Redundancy Check) は誤り検出符号の一種。Go 言語では `hash/crc32` パッケージにあるが、今回はこれも自前で実装する。PNG の仕様書に C 言語のサンプルコードが載っている ( [D. Sample CRC implementation](https://www.w3.org/TR/png/#D-CRCAppendix) ) ので、これを Go に移植する。 + +```go +var ( + crcTable [256]uint32 + crcTableComputed bool +) + +func makeCrcTable() { + for n := 0; n < 256; n++ { + c := uint32(n) + for k := 0; k < 8; k++ { + if (c & 1) != 0 { + c = 0xEDB88320 ^ (c >> 1) + } else { + c = c >> 1 + } + } + crcTable[n] = c + } + crcTableComputed = true +} + +func updateCrc(crc uint32, buf []byte) uint32 { + if !crcTableComputed { + makeCrcTable() + } + + c := crc + for n := 0; n < len(buf); n++ { + c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8) + } + return c +} + +func crc(buf []byte) uint32 { + return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF +} +``` + +できた `crc` 関数を使って、chunk 一般を書き込む関数も用意しておこう。 + +```go +func writeChunk(w io.Writer, chunkType string, data []byte) { + typeAndData := make([]byte, 0, len(chunkType)+len(data)) + typeAndData = append(typeAndData, []byte(chunkType)...) + typeAndData = append(typeAndData, data...) + + binary.Write(w, binary.BigEndian, uint32(len(data))) + binary.Write(w, binary.BigEndian, typeAndData) + binary.Write(w, binary.BigEndian, crc(typeAndData)) +} +``` + +仕様どおり、`chunkType` と `data` から CRC を計算し、`data` の長さと合わせて書き込んでいる。 +PNG では基本的に big endian を使うことに注意する。 + +準備ができたところで、具体的な chunk をエンコードしていく。 + +{#ihdr-chunk} +## IHDR chunk + +IHDR chunk は最初に配置される chunk である。次のようなデータからなる。 + +1. 画像の幅 (符号なし 4 バイト整数) +1. 画像の高さ (符号なし 4 バイト整数) +1. ビット深度 (符号なし 1 バイト整数) + + * 1 色に使うビット数。1 ピクセルに 24 bit 使う truecolor 画像では 8 になる + +1. 色タイプ (符号なし 1 バイト整数) + + * 0: グレースケール + * 2: Truecolor (今回はこれに決め打ち) + * 3: パレットのインデックス + * 4: グレースケール + アルファ + * 6: Truecolor + アルファ + +1. 圧縮方式 (符号なし 1 バイト整数) + + * PNG の仕様書に 0 しか定義されていないので 0 で固定 + +1. フィルタ方式 (符号なし 1 バイト整数) + + * PNG の仕様書に 0 しか定義されていないので 0 で固定 + +1. インターレース方式 (符号なし 1 バイト整数) + + * 今回はインターレースしないので 0 + +今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。 + +```go +import "bytes" + +func writeChunkIhdr(w io.Writer, width, height uint32) { + var buf bytes.Buffer + binary.Write(&buf, binary.BigEndian, width) + binary.Write(&buf, binary.BigEndian, height) + binary.Write(&buf, binary.BigEndian, uint8(8)) + binary.Write(&buf, binary.BigEndian, uint8(2)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + + writeChunk(w, "IHDR", buf.Bytes()) +} +``` + +{#idat-chunk} +## IDAT chunk + +IDAT chunk は、実際の画像データが格納された chunk である。IDAT chunk は deflate アルゴリズムにより圧縮され、zlib 形式で格納される。 + +{#zlib} +### Zlib + +まずは zlib について確認する。おおよそ次のような構造になっている。 + +1. 固定で 0x78 (符号なし 1 バイト整数) +1. 固定で 0x01 (符号なし 1 バイト整数) +1. データ +1. データの Adler-32 + +最初の 2 バイトにも意味はあるが、PNG では固定で構わない。 + +Adler-32 も CRC と同じく誤り検出符号である。こちらも zlib の仕様書に C 言語でサンプルコードが記載されている ( [9. Appendix: Sample code](https://www.rfc-editor.org/rfc/rfc1950#section-9) ) ので、Go に移植する。 + +```go +const adler32Base = 65521 + +func updateAdler32(adler uint32, buf []byte) uint32 { + s1 := adler & 0xFFFF + s2 := (adler >> 16) & 0xFFFF + + for n := 0; n < len(buf); n++ { + s1 = (s1 + uint32(buf[n])) % adler32Base + s2 = (s2 + s1) % adler32Base + } + return (s2 << 16) + s1 +} + +func adler32(buf []byte) uint32 { + return updateAdler32(1, buf) +} +``` + +「データ」の部分には圧縮したデータが入るのだが、真面目に deflate アルゴリズムを実装する必要はない。Zlib には無圧縮のデータブロックを格納することができるので、これを使う。本来は、データの圧縮効率の悪いランダムなデータをそのまま格納するためのものだが、今回は deflate の実装をサボるために使う。 + +1 つの無圧縮ブロックには 65535 (2^16^ - 1) バイトまで格納できる。それぞれのブロックは次のような構成になっている。 + +1. 最終ブロックなら 1、そうでなければ 0 (符号なし 1 バイト整数) +1. ブロックのバイト長 (符号なし 2 バイト整数) +1. ブロックのバイト長の 1 の補数、あるいはビット反転 (符号なし 2 バイト整数) +1. データ (最大 65535 バイト) + +実際にこの手抜き zlib を実装したものがこちら: + +```go +func encodeZlib(data []byte) []byte { + var buf bytes.Buffer + + binary.Write(&buf, binary.BigEndian, uint8(0x78)) + binary.Write(&buf, binary.BigEndian, uint8(0x01)) + blockSize := 65535 + isFinalBlock := false + for i := 0; !isFinalBlock; i++ { + var block []byte + if len(data) <= (i+1)*blockSize { + block = data[i*blockSize:] + isFinalBlock = true + } else { + block = data[i*blockSize : (i+1)*blockSize] + } + binary.Write(&buf, binary.BigEndian, isFinalBlock) + binary.Write(&buf, binary.LittleEndian, uint16(len(block))) + binary.Write(&buf, binary.LittleEndian, uint16(^len(block))) + binary.Write(&buf, binary.LittleEndian, block) + } + binary.Write(&buf, binary.BigEndian, adler32(data)) + + return buf.Bytes() +} +``` + +{#image-data} +### 画像データ + +では次に、zlib 形式で格納するデータを用意する。PNG 画像は次のような順にスキャンする。 +画像の左上のピクセルから同じ行を横にスキャンしていき、一番右まで到達したら次の行の左に向かう。 +右下のピクセルまで行けば終わり。要は Z 字型に進んでいく。 + +また、それぞれの行の先頭には、圧縮のためのフィルタタイプを指定する。 +ただ、今回はその実装を省略するために、常にフィルタ 0 (何も加工しない) を使う。 + +先ほどの `encodeZlib` も使って実際に実装したものがこちら: + +```go +func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) { + var pixels bytes.Buffer + for y := uint32(0); y < height; y++ { + binary.Write(&pixels, binary.BigEndian, uint8(0)) + for x := uint32(0); x < width; x++ { + r, g, b, _ := img.At(int(x), int(y)).RGBA() + binary.Write(&pixels, binary.BigEndian, uint8(r)) + binary.Write(&pixels, binary.BigEndian, uint8(g)) + binary.Write(&pixels, binary.BigEndian, uint8(b)) + } + } + + writeChunk(w, "IDAT", encodeZlib(pixels.Bytes())) +} +``` + +{#iend-chunk} +## IEND chunk + +最後に IEND chunk を書き込む。これは PNG 画像の最後に配置される chunk で、PNG のデコーダはこの chunk に出会うとそこでデコードを停止する。 + +特に追加のデータはなく、必要なのは chunk type の `IEND` くらいなので実装は簡単: + +```go +func writeChunkIend(w io.Writer) { + writeChunk(w, "IEND", nil) +} +``` + +{#outro} +# おわりに + +最後に全ソースコードを再掲しておく。 + +```go +package main + +import ( + "bytes" + "encoding/binary" + "image" + _ "image/png" + "io" + "os" +) + +func main() { + inFile, err := os.Open("input.png") + if err != nil { + panic(err) + } + defer inFile.Close() + + img, _, err := image.Decode(inFile) + if err != nil { + panic(err) + } + + outFile, err := os.Create("output.png") + if err != nil { + panic(err) + } + defer outFile.Close() + + writePng(outFile, img) +} + +func writePng(w io.Writer, img image.Image) { + width := uint32(img.Bounds().Dx()) + height := uint32(img.Bounds().Dy()) + writeSignature(w) + writeChunkIhdr(w, width, height) + writeChunkIdat(w, width, height, img) + writeChunkIend(w) +} + +func writeSignature(w io.Writer) { + sig := [8]uint8{ + 0x89, + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // CR + 0x0A, // LF + 0x1A, // EOF (^Z) + 0x0A, // LF + } + binary.Write(w, binary.BigEndian, sig) +} + +func writeChunkIhdr(w io.Writer, width, height uint32) { + var buf bytes.Buffer + binary.Write(&buf, binary.BigEndian, width) + binary.Write(&buf, binary.BigEndian, height) + binary.Write(&buf, binary.BigEndian, uint8(8)) + binary.Write(&buf, binary.BigEndian, uint8(2)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + binary.Write(&buf, binary.BigEndian, uint8(0)) + + writeChunk(w, "IHDR", buf.Bytes()) +} + +func writeChunkIdat(w io.Writer, width, height uint32, img image.Image) { + var pixels bytes.Buffer + for y := uint32(0); y < height; y++ { + binary.Write(&pixels, binary.BigEndian, uint8(0)) + for x := uint32(0); x < width; x++ { + r, g, b, _ := img.At(int(x), int(y)).RGBA() + binary.Write(&pixels, binary.BigEndian, uint8(r)) + binary.Write(&pixels, binary.BigEndian, uint8(g)) + binary.Write(&pixels, binary.BigEndian, uint8(b)) + } + } + + writeChunk(w, "IDAT", encodeZlib(pixels.Bytes())) +} + +func encodeZlib(data []byte) []byte { + var buf bytes.Buffer + + binary.Write(&buf, binary.BigEndian, uint8(0x78)) + binary.Write(&buf, binary.BigEndian, uint8(0x01)) + blockSize := 65535 + isFinalBlock := false + for i := 0; !isFinalBlock; i++ { + var block []byte + if len(data) <= (i+1)*blockSize { + block = data[i*blockSize:] + isFinalBlock = true + } else { + block = data[i*blockSize : (i+1)*blockSize] + } + binary.Write(&buf, binary.BigEndian, isFinalBlock) + binary.Write(&buf, binary.LittleEndian, uint16(len(block))) + binary.Write(&buf, binary.LittleEndian, uint16(^len(block))) + binary.Write(&buf, binary.LittleEndian, block) + } + binary.Write(&buf, binary.BigEndian, adler32(data)) + + return buf.Bytes() +} + +func writeChunkIend(w io.Writer) { + writeChunk(w, "IEND", nil) +} + +func writeChunk(w io.Writer, chunkType string, data []byte) { + typeAndData := make([]byte, 0, len(chunkType)+len(data)) + typeAndData = append(typeAndData, []byte(chunkType)...) + typeAndData = append(typeAndData, data...) + + binary.Write(w, binary.BigEndian, uint32(len(data))) + binary.Write(w, binary.BigEndian, typeAndData) + binary.Write(w, binary.BigEndian, crc(typeAndData)) +} + +var ( + crcTable [256]uint32 + crcTableComputed bool +) + +func makeCrcTable() { + for n := 0; n < 256; n++ { + c := uint32(n) + for k := 0; k < 8; k++ { + if (c & 1) != 0 { + c = 0xEDB88320 ^ (c >> 1) + } else { + c = c >> 1 + } + } + crcTable[n] = c + } + crcTableComputed = true +} + +func updateCrc(crc uint32, buf []byte) uint32 { + if !crcTableComputed { + makeCrcTable() + } + + c := crc + for n := 0; n < len(buf); n++ { + c = crcTable[(c^uint32(buf[n]))&0xFF] ^ (c >> 8) + } + return c +} + +func crc(buf []byte) uint32 { + return updateCrc(0xFFFFFFFF, buf) ^ 0xFFFFFFFF +} + +const adler32Base = 65521 + +func updateAdler32(adler uint32, buf []byte) uint32 { + s1 := adler & 0xFFFF + s2 := (adler >> 16) & 0xFFFF + + for n := 0; n < len(buf); n++ { + s1 = (s1 + uint32(buf[n])) % adler32Base + s2 = (s2 + s1) % adler32Base + } + return (s2 << 16) + s1 +} + +func adler32(buf []byte) uint32 { + return updateAdler32(1, buf) +} +``` + +{#references} +# 参考 + +* [Portable Network Graphics (PNG) Specification (Third Edition)](https://www.w3.org/TR/png) +* [ZLIB Compressed Data Format Specification version 3.3](https://www.rfc-editor.org/rfc/rfc1950) diff --git a/services/blog/content/posts/2023-04-04/phperkaigi-2023-report.dj b/services/blog/content/posts/2023-04-04/phperkaigi-2023-report.dj new file mode 100644 index 00000000..e4047c79 --- /dev/null +++ b/services/blog/content/posts/2023-04-04/phperkaigi-2023-report.dj @@ -0,0 +1,141 @@ +--- +[article] +uuid = "21ce39f0-d613-45f2-a760-89c368892d77" +title = "PHPerKaigi 2023 参加レポ" +description = "2023-03-23 から 2023-03-25 にかけて開催された、PHPerKaigi 2023 に参加した。" +tags = [ + "conference", + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2023-04-04" +remark = "公開" + +[[article.revisions]] +date = "2023-06-28" +remark = "トークセッションの記事版の執筆を中止" +--- +{#intro} +# はじめに + +2023-03-23 から 2023-03-25 にかけて開催された、 [PHPerKaigi 2023](https://phperkaigi.jp/2023/) に参加した。 +今年は 2つのセッションのスピーカーとして、また、当日スタッフとして参加した。 + +昨年、一昨年の参加レポはこちら: + +* [PHPerKaigi 2022](/posts/2022-05-01/phperkaigi-2022/) +* [PHPerKaigi 2021](/posts/2021-03-30/phperkaigi-2021/) + +{#as-speaker} +# スピーカーとして + +これまでとの最大の違いとして、今回はスピーカーとして登壇した。まずはそれについて書く。2つのセッションで登壇した。 + +* 詳説「参照」:PHP 処理系の実装から参照を理解する + + * [プロポーザル](https://fortee.jp/phperkaigi-2023/proposal/95e4dd94-5fc7-40fe-9e1a-230e36404cbe) + * [スライド](/slides/2023-03-24/phperkaigi-2023/) + * 解説記事 (執筆中) → 追記: 記事版の執筆は諦めた + +* PHPerチャレンジ解説セッション - デジタルサーカス株式会社 + + * [プロポーザル](https://fortee.jp/phperkaigi-2023/proposal/524c9dca-1d70-4b32-a939-9c73ffe5cb48) + * [スライド](/slides/2023-03-25/phperkaigi-2023-tokens/) + * 解説記事 (執筆中) → 追記: 記事版の執筆は諦めた + +PHPer チャレンジの話については後述する。 +参照については、PHP を書き始めた頃からずっと疑問に思っていたので、仕組みを理解する良い機会となった。 + +{#as-staff} +# 当日スタッフとして + +今回はスピーカーのみならず当日スタッフとしても参加した。 +カンファレンスのスタッフとしての参加は初めてだったが、初参加のスタッフでもスムーズに作業ができるような仕組みが整えられていた。 + +PHPerKaigi は一般参加者の目線でもよくできたカンファレンスだなあという印象だったのだが、よりその思いを強くした。 +なんとスタッフにとってもよくできたカンファレンスなのである。 + +反省点は私自身の最大 HP がまったく足りていなかったことで、次の機会には最後まで動けるようにしたいところである。 + +{#as-attendee} +# 参加者として + +{#recommended-sessions} +## おすすめセッション + +5つのセッションを厳選した。 + +[ブラウザの向こう側で「200 OK」を返すまでに何が起きているのか調べてみた](https://fortee.jp/phperkaigi-2023/proposal/f7f2f18a-e6b0-47e4-ade0-e324f72428ae) + +Web に関わるなら、バックエンドでもフロントエンドでも知っておいてほしい知識。 +タイトルを見て「こんな話だろうな」と想像がつくレベルなら見なくてもいいかも。 + +[PHPで学ぶ "Cacheの距離" の話](https://fortee.jp/phperkaigi-2023/proposal/280706e0-7158-4237-8202-c9d64330b96f) + +これも上セッションと同様に、基礎を抑えられる良いセッション。 + +[防衛的 PHP: 多様性を生き抜くための PHP 入門](https://fortee.jp/phperkaigi-2023/proposal/ad3ba31c-0214-4557-a0df-3755db8ed8cc) + +静的解析ツールの話。静的解析は PHP のみならず最近の動的言語の一大潮流なので、逃れられない。 + +[PHPの最高機能、配列を捨てよう!!](https://fortee.jp/phperkaigi-2023/proposal/e00788a4-ef25-49ee-b254-9d2b53e19633) + +実はこれも上のセッションと同様の話。 +PHP の静的解析ツールは配列にも (無理矢理) 型が付けられるものが多いが、実行時にも検査できるという点において専用のクラスを作る方が優れている。 + +[時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる](https://fortee.jp/phperkaigi-2023/proposal/7e212cb2-be37-43e8-b6ee-5236d259fcbf) + +個人的に最も楽しみにしていたセッションであり、今回のモリアガリトーク賞 (盛り上がったセッションに運営側から贈られる賞) でもある。 +ネタバレになるが、最終的に (Go で実装された) 本戦優勝スコアを超えている。 + +{#phper-challenge} +## PHPer チャレンジ + +昨年に引き続き、弊社デジタルサーカス株式会社からのトークン問題の作題を担当した。 +また、今年はさらに作成した問題を解説するセッションにも登壇した。 +今年のトークンは、昨年の PHPerKaigi 2022 が終わった段階から作り始め、約半年かけて制作した。 + +問題の制作中は大変楽しかったが、まあやりすぎた。 +いかに超絶技巧を凝らすかに注力してしまい、解く楽しさという観点を失ってしまったきらいがある。 + +(WIP: 解説ブログ記事執筆中。終わったらここにリンク) + +{#random-thoughts} +## 雑多な感想 + +なんかいろいろ。 + +* マカロンおいしかった +* \\ペチパー/ +* 名札便利 +* \\ペチパー/ +* 傘袋便利 +* \\ペチパー/ +* パーカーのデザイン良き + +(あとから見返して自分でもわけがわからなくなりそうなので書いておくと、会場に入場する際に名札をタッチすると小桜エツコさんの声で「ペチパー」という音声が流れるギミックがあった) + +{#outro} +# おわりに + +[去年の参加レポ](/posts/2022-05-01/phperkaigi-2022/#section--next-year) では、来年の目標として次を挙げた。 + +> * プロポーザルを出す +> * PHPer チャレンジのトークン問題を 5題作成する +> * 現地に行く +> * PHPer チャレンジで圧勝する + +プロポーザルに関しては採択されて登壇できたし、PHPer チャレンジは解説もおこなった。また、現地に行くだけでなく、当日スタッフとして参加した。 +4つ目の PHPer チャレンジに関しては、今年は参加していない。 +スタッフをやりながらだと入力する時間も探す時間も取れそうになかったのと、スタッフをやっている関係で少しだけ早く入手してしまうトークンがいくつか存在していたため。 + +カンファレンス全体の感想についてだが、大規模なカンファレンスにオフラインで参加するのは今回が初めてだったので、その話をしたい。 + +オンラインとオフラインだと体験が別物になる。そもそもが似て非なるものなのだ。 +向き不向きはあるだろうが、オンラインしか参加したことのないという方は、一度現地参加してみてはいかがだろうか。 + +さて、参加レポは去年も一昨年もこの言葉で締め括っているので、今年もそれで終わろうと思う。 + +ではまた来年。 diff --git a/services/blog/content/posts/2023-06-25/phpconfuk-2023-report.dj b/services/blog/content/posts/2023-06-25/phpconfuk-2023-report.dj new file mode 100644 index 00000000..ba1b7d6e --- /dev/null +++ b/services/blog/content/posts/2023-06-25/phpconfuk-2023-report.dj @@ -0,0 +1,64 @@ +--- +[article] +uuid = "e1568c4c-9bdd-47b9-8b39-939ade4f3ba0" +title = "PHP カンファレンス福岡 2023 参加レポ" +description = "2023-06-24 に開催された、PHP カンファレンス福岡に参加した。" +tags = [ + "conference", + "php", + "phpconfuk", +] + +[[article.revisions]] +date = "2023-06-25" +remark = "公開" +--- +{#intro} +# はじめに + +2023-06-24 に開催された、 [PHP カンファレンス福岡 2023](https://phpcon.fukuoka.jp/2023/) に参加した。 +また、その前日に催された、 [非公式の前夜祭](https://connpass.com/event/282285/) にも参加した。 +前夜祭では、15分の登壇もおこなった。 [登壇の方の資料はこちら。](/slides/2023-06-23/phpconfuk-2023-eve/) + +{#sessions-thoughts} +# セッションの感想 + +{#eve} +## 前夜祭 + +※セッションの題名と発表者名は、 [前夜祭イベントの connpass ページ](https://connpass.com/event/282285/) から引用。 + +* スクラム(の一部)を導入してよくなったこと (asumikam さん) + + * スクラムの「一部」を導入されたということでしたが、理想的な形で改善が進んでいるように見受けられました。特に、ブランチ運用やデプロイ頻度、フィードバックサイクルに大きく変化が起きているのは驚くべき成果だと感じました。 + +* 地方の小さな勉強会を一番の活動舞台にする (tomio さん) + + * すさまじいほどの「熱」を感じました。私自身、最近になってカンファレンスや勉強会への参加・登壇を活発におこなうようになったことで、頷く点が多かったです。 + +{#conference} +## カンファレンス + +※セッションの題名と発表者名は、 [カンファレンスの fortee ページ](https://fortee.jp/phpconfukuoka-2023/proposal/accepted) から引用。 + +* [育成力 - エンジニアの才能を引き出す環境とチューターの立ち回り - (岡嵜 雄平 さん)](https://fortee.jp/phpconfukuoka-2023/proposal/df5f06e8-900e-4e71-94d7-d0c3cc57a0ac) + + * ちょうど弊チームに新規メンバがジョインしたばかりで、オンボーディングプロセスについて考えていたところの発表でした。すぐにすべてを取り入れるというわけにはいきませんが、弊社での新人育成プロセスの改善につながるヒントをいくつか得られたと思います。 + +* オブジェクト指向は本当に必要か? (たなかひさてる さん、こいほげ さん) + + * ※当日 D ホールでおこなわれたアンカンファレンスセッションのため、正式タイトル・リンクなし + * 私自身、「オブジェクト指向」については色々と言いたいことがあるのですが、だいたいツイートしたこれとこれです。 + + * 「オブジェクト指向の話は、パラダイムの異なる複数の言語に触れているかどうかで見え方がまったく異なる印象がある。OOPはどうでもいいです (※個人の感想です)」 ( [Twitter のツイートへのリンク](https://twitter.com/nsfisis/status/1672502935983656960) ) + * 「OOPは現代の言語で考える意味はほぼない古いパラダイムだよという立場ですが、OOPについてあまり大っぴらに話してると色んなところから刺されそうなんですよね (Twitterは大っぴらじゃないんですか?)」 ( [Twitter のツイートへのリンク](https://twitter.com/nsfisis/status/1672504892244787201) ) + +* [その説明、コードコメントに書く?コミットメッセージに書く?プルリクエストに書く? (おかしょい/岡田正平 さん)](https://fortee.jp/phpconfukuoka-2023/proposal/ae71f3a7-4c3c-4c87-8816-8426bcc8d325) + + * Twitter にもツイートしましたが、完全に自分の意見と一致していたので、とても共感できました。今後は社内のコードレビュー時に、こちらの資料を貼りつけることにします。 + +{#outro} +# おわりに + +居住地域から離れた場所への遠征参加は初めてだったが、大変楽しい (しかも勉強にもなる!) 体験だった。 +受け取った「熱」が冷める前に、自らの手を動かしていきたい。 diff --git a/services/blog/content/posts/2023-10-02/compile-php-runtime-to-wasm.dj b/services/blog/content/posts/2023-10-02/compile-php-runtime-to-wasm.dj new file mode 100644 index 00000000..2664b7a2 --- /dev/null +++ b/services/blog/content/posts/2023-10-02/compile-php-runtime-to-wasm.dj @@ -0,0 +1,316 @@ +--- +[article] +uuid = "0ed1ccc8-d437-481c-8cca-2131ce800cc0" +title = "PHP の処理系を Emscripten で WebAssembly にコンパイルする" +description = "PHP の処理系 (php/php-src) を Emscripten で WebAssembly にコンパイルし、任意のコードを隔離された環境で評価できるようにした。" +tags = [ + "php", + "wasm", +] + +[[article.revisions]] +date = "2023-10-02" +remark = "公開" + +[[article.revisions]] +date = "2025-04-23" +remark = "fflush() の前に改行の出力が必要だった理由と正しい実装について追記" +--- +{#intro} +# はじめに + +[Emscripten](https://emscripten.org/) を用いて [PHP の処理系](https://github.com/php/php-src) を [WebAssembly](https://developer.mozilla.org/docs/WebAssembly) にコンパイルした。機能をある程度絞ることで、思ったよりも簡単に実現できたので、備忘録として記しておく。 + +なお、この記事では Emscripten や WebAssembly とは何か知っていることを前提とする。 + +{#version} +# バージョン情報 + +この記事中で使用するソフトウェア等のバージョンを記載する。 + +* Ubuntu 22.04 on WSL2 +* Docker version 24.0.6 +* Emscripten 3.1.46 +* Node.js 20.7.0 +* PHP 8.2.10 + +なお、Docker から下は Docker 上で導入するので、ホストマシンにはインストールしなくてよい。 + +{#goal} +# 本記事のゴール + +先にこの記事のゴールを示しておく。これから示す手順のとおりに進めると、次のようなコードが動くようになる。 +このコードはこのあと使うので、`index.mjs` の名前で保存しておくこと。 + +{filename="index.mjs"} +```javascript +import { readFile } from 'node:fs/promises'; +import PHPWasm from './php-wasm.mjs' + +const code = await readFile('/dev/stdin', { encoding: 'utf-8' }); + +const { ccall } = await PHPWasm(); +const result = ccall( + 'php_wasm_run', + 'number', ['string'], + [code], +); +console.log(`exit code: ${result}`); +``` + +標準入力から与えたコードを WebAssembly にコンパイルされた PHP 処理系の上で実行している。このような `php-wasm.mjs` (とそこから呼び出される `php-wasm.wasm`) を作成する。 + +{#build} +# ビルド + +{#write-c-entrypoint} +## C のエントリポイントを書く + +先ほどのコードでも使っていたエントリポイントである `php_wasm_run` を用意する。 + +```c +#include <stdio.h> +#include <emscripten.h> +#include <Zend/zend_execute.h> +#include <sapi/embed/php_embed.h> + +int EMSCRIPTEN_KEEPALIVE php_wasm_run(const char* code) { + zend_result result; + + int argc = 1; + char* argv[] = { "php.wasm", NULL }; + + PHP_EMBED_START_BLOCK(argc, argv); + + result = zend_eval_string_ex(code, NULL, "php.wasm code", 1); + + PHP_EMBED_END_BLOCK(); + + fprintf(stdout, "\n"); + fflush(stdout); + fprintf(stderr, "\n"); + fflush(stderr); + + return result == SUCCESS ? 0 : 1; +} +``` + +ほとんどはただの PHP の公開 API を使ったコードだが、Emscripten 向けの注意点が 2点ある。 + +まずは `EMSCRIPTEN_KEEPALIVE` について。 +これは Emscripten が用意している特殊なマクロである。 +このマクロが付与されている関数は、どこからも使用されていなくともコンパイル後の WebAssembly バイナリから削除されない。 +もしこれを付け忘れると、未使用の関数とみなされ削除される。 + +次に、コードを評価したあとに呼んでいる標準出力と標準エラー出力に対する改行の出力について。 +出力バッファから出力させるためだけなら改行を出力させなくとも `fflush()` だけで事足りると考えたのだが、ないと動かなかったので追加した。 +これにより、PHP コードの出力の後ろに余分な改行が追加されてしまう。 +改行を出力せずともバッファを消費させる手段をご存知のかたはご教示願いたい。 + +{editat="2025-04-23" operation="追記"} +::: edit +`fflush()` の前に改行の出力が必要だった理由が判明したので追記する。 +これは、`index.mjs` で標準出力・標準エラー出力へ出力する方法を指定せず、デフォルトの実装に任せているため。 +Emscripten のデフォルト実装では、改行コードを出力するまで出力内容がバッファリングされ、`fflush()` が機能しない。 + +デフォルトの出力方法は `index.mjs` の中で `PHPWasm()` を呼ぶとき、`stdout`・`stderr` というオプションを渡せば変更できる。 + +```javascript +const { ccall } = await PHPWasm({ + stdout: (c) => { + if (c === null) { + // flush the standard output. + } else { + // output c to the standard output. + } + }, +}); +``` + +`c` は `null` か 1バイト符号つき整数を取り、`null` が flush 要求を意味する。 + +記事末尾のリポジトリはすでにこの変更を適用済み。`stdout` や `stderr` の完全なサンプルはそちらを参照のこと。 +::: + +{#compile-to-wasm} +## WebAssembly にコンパイルする + +それでは WebAssembly にコンパイルしていこう。ここからは `Dockerfile` 上のコマンドとして操作を示す。 + +まずは [Emscripten 公式が提供している Docker イメージ](https://hub.docker.com/r/emscripten/emsdk) を使って、PHP 処理系と先ほど示した C 言語のソースコードを WebAssembly にコンパイルする。 + +```dockerfile +FROM emscripten/emsdk:3.1.46 AS wasm-builder +``` + +次に、 [php/php-src](https://github.com/php/php-src) から PHP 処理系のソースコードを取得し、ビルドに必要な apt パッケージを取ってくる。 +有効にする拡張を増やしたいなら、ここでインストールするパッケージも増やすことになるだろう。 + +```dockerfile +RUN git clone --depth=1 --branch=php-8.2.10 https://github.com/php/php-src + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + autoconf \ + bison \ + pkg-config \ + re2c \ + && \ + : +``` + +続けて、Emscripten のツールチェインを用いて PHP 処理系をビルドする。 + +```dockerfile +RUN cd php-src && \ + ./buildconf --force && \ + emconfigure ./configure \ + --disable-all \ + --disable-mbregex \ + --disable-fiber-asm \ + --disable-cli \ + --disable-cgi \ + --disable-phpdbg \ + --enable-embed=static \ + --enable-mbstring \ + --without-iconv \ + --without-libxml \ + --without-pcre-jit \ + --without-pdo-sqlite \ + --without-sqlite3 \ + && \ + EMCC_CFLAGS='-s ERROR_ON_UNDEFINED_SYMBOLS=0' emmake make -j$(nproc) && \ + mv libs/libphp.a .. && \ + make clean && \ + git clean -fd && \ + : +``` + +ここまでと比べると少し複雑なので、それぞれ詳しく見ていこう。 + +まず、`buildconf` は PHP 処理系をビルドするときに (Emscripten とは関係なく) 使うツールである。 +このツールの最も重要な仕事は、`configure` の生成である。 + +次に `configure` するわけだが、ここで `emconfigure` を使う。 +これを使うことで、Emscripten が上手く諸々のツールチェインを WebAssembly のビルド向けに調整しながら `configure` してくれる。 + +`configure` の後ろに指定してあるフラグは、通常の PHP 処理系のビルドで使う `configure` と同じなので、詳しくはそちらの `cofigure --help` を参照していただきたい。 +ほとんどは、機能の無効化のために指定している (依存するライブラリを減らし、ビルドをより簡単にするため)。 + +通常の C のビルドなら、`configure` の次は `make` するところだが、ここでも `emmake` を使う。 +役割はほとんど `emconfigure` と同様である。 +指定してある `EMCC_CFLAGS` という環境変数は、Emscripten の C コンパイラへのフラグで、ここでは `ERROR_ON_UNDEFINED_SYMBOLS` を無効化している。 +これにより、コンパイル中に出現した解決できなかったシンボルを無視するようになる (代わりに、そのシンボルを呼ぼうとしたタイミングで実行時エラーになる)。 +すべての依存を完全に解決するのは面倒なので、あまり使わない機能については無視してもよいだろう。 + +ここまでを実行すると `libs/libphp.a` が生成される。これは後で使うので移動させている。 + +さて、PHP 処理系をライブラリ化できたので、次に先ほど載せた C のソースコードをビルドしていこう。 +`Dockerfile` と同じ場所に `php-wasm.c` という名前で保存し、次のようにする。 + +```dockerfile +COPY php-wasm.c /src/ + +RUN cd php-src && \ + emcc \ + -c \ + -o php-wasm.o \ + -I . \ + -I TSRM \ + -I Zend \ + -I main \ + ../php-wasm.c \ + && \ + mv php-wasm.o .. && \ + make clean && \ + git clean -fd && \ + : +``` + +`emcc` は `cc` (C コンパイラ/リンカ) の Emscripten 版で、`-c` は「コンパイル」の意。 +`-o` や `-I` は普通の C コンパイラと同様、出力ファイルの指定とインクルードパスの指定である。 + +`libphp.a` と `php-wasm.o` が手に入ったので、これらをリンクして WebAssembly のバイナリとそのラッパである JavaScript ファイルを生成する。 +これにも `emcc` コマンドを使う。 + +```dockerfile +RUN emcc \ + -s ENVIRONMENT=node \ + -s ERROR_ON_UNDEFINED_SYMBOLS=0 \ + -s EXPORTED_RUNTIME_METHODS='["ccall"]' \ + -s EXPORT_ES6=1 \ + -s INITIAL_MEMORY=16777216 \ + -s INVOKE_RUN=0 \ + -s MODULARIZE=1 \ + -o php-wasm.js \ + php-wasm.o \ + libphp.a \ + ; +``` + +それぞれのフラグについて解説する。 + +`-s ENVIRONMENT=node` は、生成する WebAssembly/JavaScript の実行環境を指定する。 +今回は `node` を指定しているので、Node.js 向けのファイルが生成される。 + +`-s ERROR_ON_UNDEFINED_SYMBOLS=0` についてはすでに述べたので省略する。 + +`-s EXPORTED_RUNTIME_METHODS='["ccall"]'` は、生成される JavaScript から公開される API である。 +すでに `index.mjs` で使用しているが、`ccall('関数名', '返り値の型', ['仮引数の型', ...], ['実引数', ...])` のように使う。 + +`-s EXPORT_ES6=1` は、JavaScript コードを ECMAScript 6 に準拠した module として生成する。 +これを指定することで、`require()` ではなく `import` できる JavaScript を生成させられる。 + +`-s INITIAL_MEMORY=16777216` は呼んで字のごとく。用途に合わせて適当に決めてほしい。 + +`-s INVOKE_RUN=0` は、module をロードしたときに勝手に `main()` を呼ぶかどうか (だと思う)。 +今回は `php_wasm_run()` しか使うつもりがないので切っている。 + +`-s MODULARIZE=1` は、実質的にほぼ必須のオプションであり、1 を指定することで「WebAssembly module をインスタンス化する関数」をエクスポートするような JavaScript ファイルを生成するようになる。 +これを指定しないと、生成物の JavaScript ファイルを読み込むと WebAssembly module が即座にインスタンス化されてしまい、起動のタイミングを制御できない。 + +ここまで実行すると、`php-wasm.js` と `php-wasm.wasm` が作られる。 +では、ここからはこれらの実行環境を作っていこう。 + +といっても、Node.js はビルトインで WebAssembly をサポートしているので、ほとんどやることはない。 +先ほど掲載した JavaScript のコードは、`Dockerfile` と同じディレクトリに `index.mjs` で配置すること。 + +```dockerfile +FROM node:20.7 + +WORKDIR /app +COPY --from=wasm-builder /src/php-wasm.js /app/php-wasm.mjs +COPY --from=wasm-builder /src/php-wasm.wasm /app/php-wasm.wasm +COPY index.mjs /app/ + +ENTRYPOINT ["node", "index.mjs"] +``` + +{#run} +# 実行 + +`Dockerfile`、`php-wasm.c`、`index.mjs` を用意したら、Docker コンテナをビルドして実行する。 + +``` +$ docker build -t php-wasm . +$ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm +Hello, World! + + +exit code: 0 +``` + +{#outro} +# まとめ + +[ここまでをまとめた Git リポジトリ](https://github.com/nsfisis/tiny-php.wasm) を用意した。 +簡単にコンパイルできるので、興味があれば試してみてほしい。 + +{#references} +# 参考リンク + +* [php/php-src: ビルドの方法について](https://github.com/php/php-src) +* [Emscripten: チュートリアル](https://emscripten.org/docs/getting_started/Tutorial.html) +* [Emscripten: ビルドの基本](https://emscripten.org/docs/compiling/Building-Projects.html#building-projects) +* [Emscripten: `emcc` などのリファレンス](https://emscripten.org/docs/tools_reference/emcc.html#emccdoc) +* [Emscripten: 生成される JavaScript の API](https://emscripten.org/docs/api_reference/module.html#module) diff --git a/services/blog/content/posts/2023-10-13/i-entered-the-open-university-of-japan.dj b/services/blog/content/posts/2023-10-13/i-entered-the-open-university-of-japan.dj new file mode 100644 index 00000000..1347d901 --- /dev/null +++ b/services/blog/content/posts/2023-10-13/i-entered-the-open-university-of-japan.dj @@ -0,0 +1,22 @@ +--- +[article] +uuid = "78419bf2-a1e6-421f-875b-3d93e777b04f" +title = "放送大学に入学しました" +description = "放送大学に入学しました。頑張ります。" +tags = [ + "ouj", +] + +[[article.revisions]] +date = "2023-10-13" +remark = "公開" +--- +{#i-entered-ouj} +# 放送大学に入学しました + +とあるきっかけがあり、もう一度大学生をすることにしました。 +仕事のほうも、これまでどおりフルタイムで続けていきます。 + +黙っているよりも公表したほうがモチベーションの向上に繋がるだろうと思い、このブログに記事として載せました。 + +以上、短いですが報告でした。 diff --git a/services/blog/content/posts/2023-12-03/isucon-13.dj b/services/blog/content/posts/2023-12-03/isucon-13.dj new file mode 100644 index 00000000..991ef433 --- /dev/null +++ b/services/blog/content/posts/2023-12-03/isucon-13.dj @@ -0,0 +1,75 @@ +--- +[article] +uuid = "d0c404bb-4700-4a6f-9911-621e9872d8c6" +title = "ISUCON 13 に参加した" +description = "ISUCON 13 に参加した。チーム名「うつしもゆ」、最終スコア 13,580 点" +tags = [ + "isucon", +] + +[[article.revisions]] +date = "2023-12-03" +remark = "公開" +--- +{#intro} +# はじめに + +先日 11月25日、 [ISUCON 13](https://isucon.net/archives/57801192.html) に参加した。 +ISUCON への参加は今回が初めてとなる。 +私 nsfisis の1人チーム「うつしもゆ」として参加し、最終スコアは 13,580 点だった。使用言語は Go。 + +::: note +「ISUCON」は、LINEヤフー株式会社の商標または登録商標です。 [ISUCON 公式サイトはこちら。](https://isucon.net/) +::: + +{#goals} +# 目標 + +今回は初参加ということもあり、目標を以下のように定めた。 + +* 正のスコアを取る + + * ISUCON ではサーバ動作の整合性がチェックされ、失敗するとスコア 0 となる + +* 速度改善以外に時間を浪費しない (= ハマらない) + + * プロビジョニング、デバッグ、ミドルウェアの設定方法の調査など、性能改善に寄与しない時間を最小限にする + +{#strategy} +# 戦略 + +ISUCON で高スコアを出す戦略については、戦闘力の高い方々が良質な記事を書いてくださっている。 +ここでは、上述したような低い目標を達成するための戦略について書こうと思う。 + +{#do-not-destroy-environment} +## 環境を破壊しない + +ミドルウェアの設定やアプリケーションコードなど、変更を加えるあらゆるものは、必ずバックアップを取るか Git で管理する。 +復旧不能になって環境ごと作り直すことだけは必ず避ける。 + +{#revert-changes-immediately} +## すぐに変更を取り消す + +それでも壊してしまったときは、即座に変更を取り消す。壊れた理由を調べることに固執しない。 + +{#do-small-deployment} +## 小さくデプロイする + +一度に複数の変更を加えず、可能な限り小さな単位でデプロイする。そしてその都度ベンチマークを走らせ、整合性チェックが通るかどうかを (当然速くなっているかどうかも) 確かめる。 + +{#use-familiar-tools} +## 使い慣れた道具を使う + +使用する言語、ミドルウェア、ツール類を、使い慣れたものに限定する。 +「このツールのオプションはほとんどそらで指定できる」と言えるようなものだけを使う。 +「自分では使ったことがないが ISUCON 強者がお勧めしていた」といった理由でツールを選定しない (もちろん、本番までに練習して習熟するという選択肢は存在する)。 + +{#performance-optimization} +# パフォーマンスの最適化 + +もっと強い人の記事を参考にしてほしい。 + +{#outro} +# おわりに + +事前の準備も含めて、大変楽しいイベントだった。次回があるなら是非また参加したい。その際は、順位やスコアを目標として立てられるようになりたいものである。 diff --git a/services/blog/content/posts/2023-12-31/2023-reflections.dj b/services/blog/content/posts/2023-12-31/2023-reflections.dj new file mode 100644 index 00000000..61c09ab7 --- /dev/null +++ b/services/blog/content/posts/2023-12-31/2023-reflections.dj @@ -0,0 +1,76 @@ +--- +[article] +uuid = "bfdeed72-dd32-4d55-887f-ba004701ff4c" +title = "2023年の振り返り" +description = "2023年にやったことを振り返る" +tags = [ +] + +[[article.revisions]] +date = "2023-12-31" +remark = "公開" +--- +{#intro} +# はじめに + +男もすなる年末の振り返りといふものを女もしてみむとてするなり。 + +{#conferences} +# 登壇・カンファレンススタッフ + +勉強会やカンファレンスで登壇したりスタッフをしたりし始めたのは今年かららしい。 +LT 等も含めて計 11 回の登壇をおこなった。 + +* PHP 勉強会@東京での登壇 (計 8 回) + + * [第 148 回](/slides/2023-01-18/phpstudy-tokyo-148/) + * [第 149 回](/slides/2023-02-15/phpstudy-tokyo-149/) + * [第 150 回](/slides/2023-03-15/phpstudy-tokyo-150/) + * [第 151 回](/slides/2023-04-12/phpstudy-tokyo-151/) + * [第 153 回](/slides/2023-06-21/phpstudy-tokyo-153/) + * [第 154 回](/slides/2023-07-26/phpstudy-tokyo-154/) + * [第 155 回](/slides/2023-08-24/phpstudy-tokyo-155/) + * [第 157 回](/slides/2023-10-25/phpstudy-tokyo-157/) + +* PHPerKaigi 2023 での登壇 + + * [レギュラートーク](/slides/2023-03-24/phperkaigi-2023/) + * [トークン解説セッション](/slides/2023-03-25/phperkaigi-2023-tokens/) + +* PHPerKaigi 2023 での当日スタッフ業 +* [非公式でおこなわれた PHP カンファレンス福岡 2023 の 前夜祭イベントでの登壇](/slides/2023-06-23/phpconfuk-2023-eve/) +* PHPerKaigi 2024 でのコアスタッフ業 + +{#articles} +# 書いた記事 + +登壇が増えたためか記事を書く機会が減ってしまった。 +特に社内記事の本数が大きく減少しており、一昨年は約 100 本、昨年は約 60 本の社内記事を書いていたが、今年は 30 本強に留まった。 +その頃と比べると文章を書く筋肉が衰えているように感じる。 + +* 社外記事 (このブログ): 8本 +* 社内記事: 34本 + + * 年間で最も記事を書いた人として社内表彰された + +{#coding} +# 作ったもの + +ガラクタをいくつか作った。役には立たないが、作るのが楽しいという効用がある。 + +* [PHPerKaigi2023-tokens](https://github.com/nsfisis/PHPerKaigi2023-tokens) : PHPerKaigi 2023 でおこなわれた PHPer チャレンジという企画で用意した問題 +* [twitter2x-quine](https://github.com/nsfisis/twitter2x-quine) : Twitter のロゴを 𝕏 にする変則 quine +* [9-puzzle-quine.php](https://github.com/nsfisis/9-puzzle-quine.php) : 9パズルが遊べる変則 quine + +{#misc} +# その他 + +* [放送大学に入学した](/posts/2023-10-13/i-entered-the-open-university-of-japan/) +* [ISUCON に初参加した](/posts/2023-12-03/isucon-13/) +* データベーススペシャリストを取得した +* 漢検2級を取得した + +{#outro} +# おわりに + +今年も大変お世話になりました。よいお年を! diff --git a/services/blog/content/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file.dj b/services/blog/content/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file.dj new file mode 100644 index 00000000..483b0b9a --- /dev/null +++ b/services/blog/content/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file.dj @@ -0,0 +1,205 @@ +--- +[article] +uuid = "05cb16e1-05bc-4359-bc06-88ac20510740" +title = "【Neovim】 空の PHP ファイルに namespace 宣言を挿入する" +description = "Neovim で空の PHP ファイルを開いたとき、ディレクトリの構造に基づいて自動的に namespace 宣言を挿入するようにする。" +tags = [ + "neovim", + "php", +] + +[[article.revisions]] +date = "2024-01-10" +remark = "公開" +--- +::: note +この記事は [Vim 駅伝](https://vim-jp.org/ekiden/) #136 の記事です。 +::: + +{#intro} +# やりたいこと + +Neovim で空の PHP ファイルを開いたとき、そのファイルが置かれているディレクトリの構造に基づいて、自動的に `namespace` 宣言を挿入したい。具体的には、トップレベルの名前空間が `MyNamespace` であり、ファイル `src/Foo/Bar/Baz.php` を開いたときに、そのファイルが空であるなら、次のようなテンプレートが自動的に挿入されてほしい。 + +```php +<?php + +namespace MyNamespace\Foo\Bar; +``` + +{#version} +# バージョン情報 + +``` +$ nvim --version +NVIM v0.9.2 +Build type: Release +LuaJIT 2.1.1693350652 +``` + +今回は Lua で処理を記述したため、Vim では動作しない。以下の説明でも Neovim に絞って述べる。 +また、パス区切りがスラッシュである前提で記述したため、Windows には対応していない。 + +{#ftplugin} +# ftplugin を用意する + +Neovim には特定のファイルタイプに対して特別な処理をおこなうための ftplugin と呼ばれる仕組みがある。 +Neovim の設定を置くディレクトリ (例えば `~/.config/nvim`) の配下に `ftplugin/<FILE_TYPE>.vim` または `ftplugin/<FILE_TYPE>.lua` というファイルを配置すると、その `<FILE_TYPE>` が読み込まれたときにそのファイルが自動的に実行される。 + +今回は、Neovim がデフォルトで用意している PHP 用 ftplugin が動作したあとに追加の処理をおこないたいので、`after/ftplugin/php.{vim,lua}` というファイルを配置する。名前から察せられるとおり、`after/ftplugin` 以下のファイルは `ftplugin` 以下のファイルよりもあとに実行される。 + +この記事では Lua で処理を記述するため、拡張子には `.lua` を用いる。 +これ以降載せるコードは、すべて `after/ftplugin/php.lua` の中に記述している。 + +{#did-ftplugin} +# 二重読み込みを防ぐ + +ファイルタイプは読み込んだあとに変更されることもあるので、ftplugin は複数回実行されうる。 +二重読み込みを防ぐために、`did_ftplugin_<FILE_TYPE>_after` というバッファローカル変数を定義しておくのが慣習となっている。 + +```lua +if vim.b.did_ftplugin_php_after then + return +end + +-- ここに実際の処理を書く + +vim.b.did_ftplugin_php_after = true +``` + +{#implement} +# 実装する + +では実装していこう。今回私は次のようなロジックとした。以降、「今 Neovim で開いた PHP ファイル」のことを「対象ファイル」と呼ぶことにする。 + +1. 対象ファイルが空でなければ何もしない +1. 対象ファイルが置かれたディレクトリを上に辿って、`composer.json` を見つける +1. `composer.json` の `autoload.psr-4` を見て、トップレベルの名前空間とディレクトリを特定する +1. 対象ファイルが置かれたディレクトリが、トップレベルのディレクトリを基準としてどのようにネストしているか調べる +1. オートロードの設定と照らし合わせて、対象ファイルが属すべき名前空間を特定する +1. PHP の開始タグとともに `namespace` 宣言を挿入する + +実装を簡単にするため、Composer を用いない場合や PSR 4 以外のオートロード規則を使う場合には対応しない。少々長くなるが、以下にスクリプト全文を載せる。 + +```lua +if vim.b.did_ftplugin_php_after then + return +end + +-- base_dir を起点としてディレクトリを上向きに辿っていき、composer.json を探す +-- :help vim.fs.find() +local function find_composer_json(base_dir) + return vim.fs.find('composer.json', { + path = base_dir, + upward = true, + -- ホームディレクトリまで到達したら探索を打ち切る + stop = vim.loop.os_homedir(), + type = 'file', + })[1] +end + +-- JSON ファイルを読み込み、デコードして返す +-- :help readblob() +-- :help vim.json.decode +-- :help luaref-pcall() +local function load_json(file_path) + -- readblob() は Vim script では Blob オブジェクトを返すが、Lua から呼ぶと string に変換される + local ok_read, content = pcall(vim.fn.readblob, file_path) + if not ok_read then + return nil + end + local ok_decode, obj = pcall(vim.json.decode, content) + if not ok_decode then + return nil + end + return obj +end + +-- 対象ファイルの置かれたディレクトリを基に namespace 宣言を生成する +-- :help nvim_buf_get_name() +-- :help vim.fs.dirname() +local function generate_namespace_declaration() + -- composer.json を探し、トップレベルの名前空間とディレクトリを特定する + local current_dir = vim.fs.dirname(vim.api.nvim_buf_get_name(0)) + local path_to_composer_json = find_composer_json(current_dir) + if not path_to_composer_json then + return nil -- failed to locate composer.json + end + local composer_json = load_json(path_to_composer_json) + if not composer_json then + return nil -- failed to load composer.json + end + -- autoload.psr-4 を探し、型が期待される型と一致するかどうか調べる + local psr4 = vim.tbl_get(composer_json, 'autoload', 'psr-4') + if not psr4 then + return nil -- autoload.psr-4 section is absent + end + if vim.tbl_count(psr4) ~= 1 then + return nil -- psr-4 section is ambiguous + end + local psr4_namespace, psr4_dir + for k, v in pairs(psr4) do + psr4_namespace = k + psr4_dir = v + end + if type(psr4_dir) == 'table' then + if #psr4_dir == 1 then + psr4_dir = psr4_dir[1] + else + return nil -- psr-4 section is ambiguous + end + end + if type(psr4_namespace) ~= 'string' or type(psr4_dir) ~= 'string' then + return nil -- psr-4 section is invalid + end + -- 末尾のスラッシュとバックスラッシュを取り除いておく + if psr4_namespace:sub(-1, -1) == '\\' then + psr4_namespace = psr4_namespace:sub(0, -2) + end + if psr4_dir:sub(-1, -1) == '/' then + psr4_dir = psr4_dir:sub(0, -2) + end + + -- 対象ファイルが置かれたディレクトリとトップレベルのディレクトリを比較し、その差分を名前空間とする + local namespace_root_dir = vim.fs.dirname(path_to_composer_json) .. '/' .. psr4_dir + if not vim.startswith(current_dir, namespace_root_dir) then + return nil + end + local current_path_suffix = current_dir:sub(#namespace_root_dir + 1) + local namespace = psr4_namespace .. current_path_suffix:gsub('/', '\\') + return ("namespace %s;"):format(namespace) +end + +local function generate_template() + local lines = { + '<?php', + '', + 'declare(strict_types=1);', + '', + } + local namespace_decl = generate_namespace_declaration() + if namespace_decl then + lines[#lines + 1] = namespace_decl + lines[#lines + 1] = '' + end + lines[#lines + 1] = '' + return lines +end + +if vim.fn.line('$') == 1 and vim.fn.getline(1) == '' then + -- 対象ファイルが空なら、テンプレートを挿入してカーソルを末尾に移動させる + -- :help setline() + -- :help cursor() + vim.fn.setline(1, generate_template()) + vim.fn.cursor('$', 0) +end + +vim.b.did_ftplugin_php_after = true +``` + +{#outro} +# おわりに + +簡易的な実装だが、多くのケースではうまく動いているようだ。 +最大の問題は PSR 4 に準拠しないフレームワークを用いているとまったく役に立たないことで、今まさに職場で困っている。 +こちらはいずれ改良したい。 diff --git a/services/blog/content/posts/2024-02-03/install-wireguard-on-personal-server.dj b/services/blog/content/posts/2024-02-03/install-wireguard-on-personal-server.dj new file mode 100644 index 00000000..89ecd7b4 --- /dev/null +++ b/services/blog/content/posts/2024-02-03/install-wireguard-on-personal-server.dj @@ -0,0 +1,144 @@ +--- +[article] +uuid = "210673d0-c19e-4195-a280-968a0729dd41" +title = "【備忘録】 個人用サーバに WireGuard を導入する" +description = "個人用サービスのセルフホストに使っているサーバに WireGuard を導入する作業をしたメモ" +tags = [ + "note-to-self", + "wireguard", +] + +[[article.revisions]] +date = "2024-02-03" +remark = "公開" + +[[article.revisions]] +date = "2024-02-17" +remark = "80 番ポートについて追記" +--- +{#intro} +# はじめに + +個人用サービスのセルフホストに使っているサーバに [WireGuard](https://www.wireguard.com/) を導入する作業をしたのでメモ。 + +登場するホストは以下のとおり: + +* サーバ (Ubuntu): `10.10.1.1` +* クライアント 1 (Windows): `10.10.1.2` +* クライアント 2 (Android): `10.10.1.3` + +後ろの IP アドレスは VPN 内で使用するプライベート IP アドレス。 + +{#install-wireguard-server} +# WireGuard のインストール: サーバ + +まずは個人用サービスをホストしている Ubuntu のサーバに WireGuard をインストールする。 + +``` +$ sudo apt install wireguard +``` + +次に、WireGuard で使用する鍵を生成する。 + +``` +$ wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub +$ sudo chmod 600 /etc/wireguard/server.{key,pub} +``` + +{#install-wireguard-client} +# WireGuard のインストール: クライアント + +公式サイトから各 OS 向けのクライアントソフトウェアを入手し、インストールする。次に、設定をおこなう。 + +```ini +# クライアント 1 の場合 +[Interface] +Address = 10.10.1.2/32 +PrivateKey = <クライアント 1 の秘密鍵> + +[Peer] +PublicKey = <サーバの公開鍵> +AllowedIPs = <サーバの外部 IP アドレス>/32 +Endpoint = <サーバの外部 IP アドレス>:51820 +``` + +```ini +# クライアント 2 の場合 +[Interface] +Address = 10.10.1.3/32 +PrivateKey = <クライアント 2 の秘密鍵> + +[Peer] +PublicKey = <サーバの公開鍵> +AllowedIPs = <サーバの外部 IP アドレス>/32 +Endpoint = <サーバの外部 IP アドレス>:51820 +``` + +`PrivateKey` や `PublicKey` は鍵ファイルのパスではなく中身を書くことに注意。 + +{#configure-wireguard} +# WireGuard の設定 + +一度サーバへ戻り、WireGuard の設定ファイルを書く。 + +``` +$ sudo vim /etc/wireguard/wg0.conf +``` + +```ini +[Interface] +Address = 10.10.1.1/32 +SaveConfig = true +PrivateKey = <サーバの秘密鍵> +ListenPort = 51820 + +[Peer] +PublicKey = <クライアント 1 の公開鍵> +AllowedIPs = 10.10.1.2/32 + +[Peer] +PublicKey = <クライアント 2 の公開鍵> +AllowedIPs = 10.10.1.3/32 +``` + +次に、WireGuard のサービスを起動する。 + +``` +$ sudo systemctl enable wg-quick@wg0 +$ sudo systemctl start wg-quick@wg0 +``` + +{#configure-firewall} +# ファイアウォールの設定 + +続けてファイアウォールを設定する。まずは WireGuard が使用する UDP のポートを開き、`wg0` を通る通信を許可する。 + +``` +$ sudo ufw allow 51820/udp +$ sudo ufw allow in on wg0 +$ sudo ufw allow out on wg0 +``` + +次に、80 や 443 などの必要なポートについて、`wg0` を経由してのアクセスのみ許可する。 + +``` +$ sudo ufw allow in on wg0 to any port 80 proto tcp +$ sudo ufw allow in on wg0 to any port 443 proto tcp +``` + +最後に、`ufw` を有効にする。 + +``` +$ sudo ufw status +$ sudo ufw enable +``` + +{#connect-each-other} +# 接続する + +これで、各クライアントで VPN を有効にすると、当該サーバの 80 ポートや 443 ポートにアクセスできるようになったはずだ。念のため VPN を切った状態でアクセスできないことも確認しておくとよいだろう。 + +{#edit-80-port} +# 追記: 80 番ポートについて + +Let's Encrypt でサーバの証明書を取得している場合、80 番ポートを空けておく必要がある。気づかないうちに証明書が切れないよう注意。 diff --git a/services/blog/content/posts/2024-02-10/yapcjapan-2024-report.dj b/services/blog/content/posts/2024-02-10/yapcjapan-2024-report.dj new file mode 100644 index 00000000..3153f96b --- /dev/null +++ b/services/blog/content/posts/2024-02-10/yapcjapan-2024-report.dj @@ -0,0 +1,44 @@ +--- +[article] +uuid = "230a0048-93c0-4aac-91ef-bb3108f3e587" +title = "YAPC::Hiroshima 2024 参加レポ" +description = "2024-02-10 に開催された、YAPC::Hiroshima 2024 に参加した。" +tags = [ + "conference", + "perl", + "yapc", +] + +[[article.revisions]] +date = "2024-02-10" +remark = "公開" +--- +{#intro} +# はじめに + +2024-02-10 に開催された、 [YAPC::Hiroshima 2024](https://yapcjapan.org/2024hiroshima/) に参加した。 + +{#sessions-thoughts} +# セッションの感想 + +※セッションの題名と発表者名は、 [カンファレンスの fortee ページ](https://fortee.jp/yapc-hiroshima-2024) から引用。 + +* [VISAカードの裏側と “手が掛かる” 決済システムの育て方 (三谷 さん)](https://fortee.jp/yapc-hiroshima-2024/proposal/c0e77f91-f856-48a0-9741-b9afb662cd30) + + * ベストスピーカー賞にも選ばれていましたが、大変面白い発表でした。私自身はカード決済の知識がまったくなかったのですが、巧みな説明により、「わかったような気がする」状態になれました。 + +* [awkでつくってわかる、Webアプリケーション (やんまー さん)](https://fortee.jp/yapc-hiroshima-2024/proposal/0e545260-61e1-465e-951c-91d6afb7782c) + + * ゲームでもプログラミングでも縛りプレイほど楽しいものはないと思います。発表中ではさらっと流されていましたが、データベースとの通信や TLS、GitHub の SSO など、およそ awk で書かれたとは思えぬ機能が多数実装されており、カンファレンスなどの場でしかなかなか味わうことのない狂気に触れることができました。 + +* キーノート (杜甫々 さん) + + * ※ 招待講演のため fortee のプロポーザルページなし + * 私が小学6年生のとき、プログラミングを始めようと最初に開いたのが「 [とほほの Java 入門](https://www.tohoho-web.com/java/) 」でした。私の人生の道を決定したその第一歩目のサイトの運営者が今まさに目の前で話しているというのは、感動などという言葉ではとても言い尽くせません。これだけで、広島まで来る価値があったと断言できます。 + +{#outro} +# おわりに + +最高だった。特に、杜甫々氏の講演を生で拝聴できたのは、感慨とともに大いに刺激となった。次回の YAPC にも是非参加したい。 + +P.S. Perl を書いたことがなくとも十二分に楽しめるイベントなので、「Perl を書かない」という理由で参加しなかったかたは、次回是非参加を検討してみてほしい。 diff --git a/services/blog/content/posts/2024-02-22/phpkansai-2024-report.dj b/services/blog/content/posts/2024-02-22/phpkansai-2024-report.dj new file mode 100644 index 00000000..83205e13 --- /dev/null +++ b/services/blog/content/posts/2024-02-22/phpkansai-2024-report.dj @@ -0,0 +1,44 @@ +--- +[article] +uuid = "fd8fcb03-8e4d-4ca7-8499-0674accc51a9" +title = "PHPカンファレンス関西 2024 参加レポ" +description = "2024-02-11 に開催された、PHPカンファレンス関西 2024 に参加した。" +tags = [ + "conference", + "php", + "phpkansai", +] + +[[article.revisions]] +date = "2024-02-21" +remark = "公開" +--- +{#intro} +# はじめに + +2024-02-11 に開催された、 [PHPカンファレンス関西 2024](https://2024.kphpug.jp/) に参加した。 + +{#sessions-thoughts} +# セッションの感想 + +※セッションの題名と発表者名は、 [カンファレンスの fortee ページ](https://fortee.jp/phpcon-kansai2024) から引用。 + +* [RDBアンチパターンと戦う - 削除フラグ 完全攻略ガイド (曽根 壮大 さん)](https://fortee.jp/phpcon-kansai2024/proposal/4e03491c-2a97-40aa-8ff9-a68593b0e847) + + * アンチパターンとして紙の上での知識だけあるものの、実際にどう設計すべきなのか、あるいは今すでに使われている場合にどう直していくべきなのかについては、知識がまったく足りていなかったため、よい機会となりました。データベース分野については、今後も知識のインプットと経験が必要だと感じています。 + +* [PHPコミュニティ、その魅力と熱狂をあなたにも!!! (ことみん さん)](https://fortee.jp/phpcon-kansai2024/proposal/c903c4be-77bb-47b9-85a1-5bfdfd61c1aa) + + * もしこの記事を読んでいるあなたがまだ一度もカンファレンスや勉強会に参加したことがないなら、この記事はどうでもいいのでスライドを見てください。伝えるべきことは以上です。 + +* [ほげ言語にあってPHPにない機能 (田中ひさてる さん)](https://fortee.jp/phpcon-kansai2024/proposal/0e0befdb-2028-42c8-98e2-b19e434f5a82) + + * 私はプログラミング言語の比較が大好きなので、非常に楽しかったです。UFCS (Uniform Function Call Syntax) の知名度の低さには驚きましたが、D言語er で会場が埋め尽くされていたらそれはそれで驚きなのでやむなしかもしれません。個人的に「ほげ言語にあってPHPにない機能」の中で一番ほしいのは代数的データ型です。 + +{#outro} +# おわりに + +[本カンファレンスの前日 2024-02-10 は YAPC::Hiroshima に参加しており](/posts/2024-02-10/yapcjapan-2024-report/) 、2日連続のカンファレンスとなった。かなり疲れはしたが、その分充実した週末となったように思う。 + +翌3月は PHPerKaigi 2024、4月は PHPカンファレンス小田原 2024 があり、いずれもスタッフ兼スピーカーで参加予定である。 +今度は提供する側として、満足のいくカンファレンスになるようにしたい。 diff --git a/services/blog/content/posts/2024-03-17/phperkaigi-2024-report.dj b/services/blog/content/posts/2024-03-17/phperkaigi-2024-report.dj new file mode 100644 index 00000000..65c7f70d --- /dev/null +++ b/services/blog/content/posts/2024-03-17/phperkaigi-2024-report.dj @@ -0,0 +1,88 @@ +--- +[article] +uuid = "750be5c8-ca52-4cbd-86fe-5645b06bde95" +title = "PHPerKaigi 2024 参加レポ" +description = "2024-03-07 から 2024-03-09 にかけて開催された、PHPerKaigi 2024 に参加した。" +tags = [ + "conference", + "php", + "phperkaigi", +] + +[[article.revisions]] +date = "2024-03-17" +remark = "公開" + +[[article.revisions]] +date = "2024-07-07" +remark = "Wasm ランタイムの進捗について追記" +--- +{#intro} +# はじめに + +2024-03-07 から 2024-03-09 にかけて開催された、 [PHPerKaigi 2024](https://phperkaigi.jp/2024/) に参加した。 +今年はスピーカーとして、また、コアスタッフとして参加した。 + +過去の参加レポはこちら: + +* [PHPerKaigi 2023](/posts/2023-04-04/phperkaigi-2023-report/) +* [PHPerKaigi 2022](/posts/2022-05-01/phperkaigi-2022/) +* [PHPerKaigi 2021](/posts/2021-03-30/phperkaigi-2021/) + +{#as-speaker} +# スピーカーとして + +昨年に続き、スピーカーとして登壇をおこなった。 + +* WebAssembly を理解する 〜VM の作成を通して〜 + + * [プロポーザル](https://fortee.jp/phperkaigi-2024/proposal/bc5dc153-17af-4079-8f1b-2660af97e2c8) + * [スライド](/slides/2024-03-08/phperkaigi-2024/) + +WebAssembly の VM を PHP で実装し、実装に至るまでの道程や WebAssembly の特徴、言語処理系を作る楽しさについて語った。 +タイトルにある「WebAssembly を理解する」という目的が達成できるようなトークだったかと言われると疑問は残るものの、実際に作った人にしかできない話をすることはできたと思う。 + +{#as-staff} +# コアスタッフとして + +昨年は当日スタッフとして参加したが、今年はコアスタッフとして運営に参加した。 +今年はコードゴルフ企画を提案し、その準備とシステムの開発、当日の運用をおこなった。 +そのシステムは現在も下記の URL から閲覧でき、当日出題された問題や参加者の方々の回答が見られる。 + +[Albatross.PHP](https://t.nil.ninja/phperkaigi/2024/golf/) + +システムの開発完了や問題の作成完了はスケジュールギリギリとなったのだが、当日はそこそこ安定して稼動していたのではないかと思う。 + +{#as-attendee} +# 参加者として + +{#my-best-session} +## マイベストセッション + +[RubyVM を PHP で実装する〜Hello World を出力するまで〜](https://fortee.jp/phperkaigi-2024/proposal/ac59d0dd-795a-47cb-ba59-c0b1772d00cc) (めもりー さん) + +今回一番楽しみにしていたセッションであり、期待どおりの面白さだった。 +私も今回 VM を作るというテーマで登壇したこともあり、高い解像度で受け取ることができたように思う。 + +P.S. Ask the Speaker で話した、Ruby VM (written in PHP) on PHP VM (compiled to Wasm) on Wasm VM (written in PHP) on PHP というアイデアは「マジ」なので、続報をお待ちください (自作 Wasm runtime に不足している機能を鋭意実装中です)。 + +{editat="2024-07-07" operation="追記"} +::: edit +[コミット a312e95](https://github.com/nsfisis/php-waddiwasi/commit/a312e95a95d243943535f94653822d6796d4637f) で、ついに Ruby VM on PHP VM on Wasm VM on PHP を実現した。現時点での動かしかたは README に記載している。 +::: + +{#outro} +# おわりに + +今年はスピーカーとスタッフともに開発を伴うものだったので (Wasm 処理系とコードゴルフシステム)、両者がぶつかった結果として準備段階は去年よりも大変になった。 + +[ゴリゴリに開発しなければいけないセッションのスピーカーとゴリゴリに開発しなければいけない企画のスタッフを同じカンファレンスでやってはいけない](https://twitter.com/nsfisis/status/1765366490277253502) + +ただ、それでもコアスタッフとして半年ほど関わっただけに、終わってみると感慨深い。 +例年どおり、お祭のような活気・熱気を感じることができた。 + +来月は、また登壇とスタッフ (こちらは当日スタッフ) をおこなう [PHP カンファレンス小田原](https://phpcon-odawara.jp/) があるので、良いトーク・良いカンファレンスを作れるようにしたい。 + +さて、参加レポは例年この言葉で締め括っているので、今年もそれで終わろうと思う。 + +ではまた来年。 diff --git a/services/blog/content/posts/2024-03-20/my-bucket-list.dj b/services/blog/content/posts/2024-03-20/my-bucket-list.dj new file mode 100644 index 00000000..d998cc2b --- /dev/null +++ b/services/blog/content/posts/2024-03-20/my-bucket-list.dj @@ -0,0 +1,46 @@ +--- +[article] +uuid = "6b749793-c760-4597-8a4c-b32d027b7585" +title = "死ぬまでに作る自作○○一覧あるいは人生の TODO リスト" +description = "駄文" +tags = [ +] + +[[article.revisions]] +date = "2024-03-20" +remark = "公開" + +[[article.revisions]] +date = "2024-04-07" +remark = "URL slug を todos-in-my-life から my-bucket-list へ変更" +--- + +これは眠れない夜にノートへ書き散らした文をなんとか文章の体裁に直したものであり、およそ論理と呼べるものを期待してはならぬ。 + +Knuth 曰くプログラミングは文芸である。断っておくが、労役に伴うプロダクティブでプラクティカルな行為を指してそう言っているのではない (Knuth がどう考えているかは知らないが、少なくとも私にとっては)。いわゆる趣味プログラミング、穢れなき自由意志の下で記述されるプログラムとはすなわち、絵描きにとっての絵、文字書きにとっての文章に等しい。プログラムとは、ソースコードとは、芸術作品の一形態なのである。 + +この人生でプログラミングという行為に魅せられたからには、美しい作品を遺さねばならぬ。すなわち、簡潔で、理解しやすく、凝縮され、機能的で、速く、軽く、よい名前を持ち、うまく動くものをだ。 + +何を作りたいかは各々異なるであろうが、私にとっては車輪の再発明として知られる自作○○である。 + +車輪の再発明を恐れてはいけない。これを批判する人間というのは、プロダクティビティやプラクティカリティにフォーカスするエンジニアという人種である。今私が表現者たろうとするなら、自らの手で自らの車輪を作ることに何の恐れを抱く必要があろうか。 + +そう、これが私の死ぬまでに作る自作○○一覧あるいは人生の TODO リストである (現時点ですでに部分的あるいは全面的に達成しているものを含む)。 + +* 自作 C コンパイラ +* 自作アセンブラ +* 自作リンカ +* 自作 Scheme 処理系 +* 自作 ML コンパイラ +* 自作 Lua 処理系 +* 自作 JVM +* 自作 Wasm 処理系 +* 自作正規表現エンジン +* 自作 JavaScript 処理系 +* 自作ブラウザ +* 自作エディタ +* 自作 ActivityPub 実装 + +選定理由は作りたいということのほかにない。そこに題材とキャンバスがあり絵筆と絵具があれば、生きとし生けるもの、いづれかコードを書かざりける。 + +おお、願わくは、私にこれらを生み出すだけの時間があらんことを。 diff --git a/services/blog/content/posts/2024-04-14/phpcon-odawara-2024-report.dj b/services/blog/content/posts/2024-04-14/phpcon-odawara-2024-report.dj new file mode 100644 index 00000000..3207d3d0 --- /dev/null +++ b/services/blog/content/posts/2024-04-14/phpcon-odawara-2024-report.dj @@ -0,0 +1,76 @@ +--- +[article] +uuid = "be9c896d-7efa-42dd-a50a-dda5fd3a7f5c" +title = "PHP カンファレンス小田原 2024 参加レポ" +description = "2024-04-13 に開催された、PHP カンファレンス小田原 2024 に参加した。" +tags = [ + "conference", + "php", + "phpcon-odawara", +] + +[[article.revisions]] +date = "2024-04-14" +remark = "公開" + +[[article.revisions]] +date = "2024-06-01" +remark = "セッションの感想を追加" +--- +{#intro} +# はじめに + +2024-04-13 に開催された [PHP カンファレンス小田原](https://phpcon-odawara.jp/) に、スピーカーとして、また当日スタッフとして参加した。 + +{#as-speaker} +# スピーカーとして + +PHP 処理系の JIT コンパイルにおける PHP 8.4 での変更について、登壇をおこなった。 + +* 来る新 JIT エンジンについて知った気になる + + * [プロポーザル](https://fortee.jp/phpconodawara-2024/proposal/bc9669f6-6583-489c-aa6a-1b68abf7c291) + * [スライド](/slides/2024-04-13/phpcon-odawara-2024/) + +今回、どこから話を始めるか大いに迷ったのだが、最終的には PHP 処理系の opcode や VM といった概念は既知のものとし、そこから JIT コンパイルへ繋げるといった構成にした。 + +PHP の処理系がスクリプトを opcode へ変換する過程については、ちょうど同じカンファレンスの [めもりーさんの発表](https://fortee.jp/phpconodawara-2024/proposal/21d94a60-404d-4fba-8c60-d1c8889a0138) あたりを参考にしていただくとよいだろう。 +また、新しい IR についてより詳しく知りたいという方は、スライド末尾の「参考資料」にあるリンクを参照いただくのがよいかと思う。 + +Tracing JIT の発火条件や、IR を使って実現される最適化方法など、調べたものの発表に入らなかった話がごまんとあるので、これもどこかに持っていければと考えている。 + +{#as-staff} +# スタッフとして + +当日スタッフとして前日の準備と当日の運営をおこなった。今回はモノの移動が比較的 (比較対象: [PHPerKaigi](/posts/2024-03-17/phperkaigi-2024-report/) ) 少なく、体力にはかなり余裕があった。 + +自分の担当範囲内では、一度タイムキーパー係のときに時間を思いきり間違えた以外は、スムーズに進められたかと思う。 + +また、これはコアスタッフの方々のおかげだろうが、初開催としては大きなトラブルなく終わったと言えるのではないだろうか。 + +{#as-attendee} +# 参加者として + +発表タイトルと発表者名は fortee より引用 + +* FigmaとPHPで作る、1ミリたりとも表示崩れしない最強の帳票印刷ソリューション (たつきち さん) + + * プロポーザルリンク: https://fortee.jp/phpconodawara-2024/proposal/7c57d5ca-213a-4d7a-aaf0-26ddc44897f0 + * 感想: 最初のアイデアから途中の泥臭いワークアラウンドまで非常におもしろかったです。帳票には何度か苦しめられているので、機会があれば試してみたいです。 + +* PHPの次期バージョンはこの時期どうなっているのか、Internalsの開発体制について (てきめん さん) + + * プロポーザルリンク: https://fortee.jp/phpconodawara-2024/proposal/740b034a-81f0-4b7a-90e9-cd3fa01c651f + * 感想: 前々から出そうとしている RFC があるので、RFC についての日本語情報が増えるのは大変ありがたいです。あとは作業を進めなければ......。 + +* Architecture Decision Record を一年運用してみた (富所 亮 さん) + + * プロポーザルリンク: https://fortee.jp/phpconodawara-2024/proposal/56218b4f-b724-4199-82f1-67497501a9ef + * 感想: 今回最も楽しみにしていた発表の一つです。設計指針の調査・共有等には課題を感じていたので、弊チームでも導入のために動いていこうと思います。 + +{#outro} +# おわりに + +怒涛の月刊 PHP カンファレンスも折り返しとなったが、まだまだ新鮮に楽しい。 + +また今度、カンファレンスで会いましょう (震源地がよくわかっていないのだけれど、575 が流行っているらしい)。 diff --git a/services/blog/content/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd.dj b/services/blog/content/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd.dj new file mode 100644 index 00000000..9872d284 --- /dev/null +++ b/services/blog/content/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd.dj @@ -0,0 +1,155 @@ +--- +[article] +uuid = "a4c326a6-5ffe-450c-abf2-45833c5efb6a" +title = "【GitLab】 GitLab CI/CD 上での bash/sh は pipefail が有効になっている" +description = "GitLab CI/CD で bash/sh スクリプトを動かすと、pipefail オプションが有効になった状態で実行される。" +tags = [ + "ci-cd", + "gitlab", +] + +[[article.revisions]] +date = "2022-11-17" +remark = "デジタルサーカス株式会社の社内記事として公開" +isInternal = true + +[[article.revisions]] +date = "2024-04-21" +remark = "ブログ記事として一般公開" +--- +::: note +この記事は、2022-11-17 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。 +::: + +ハマったのでメモ。 + +{#background} +# 前提 + +{#gitlab-ci-cd} +## GitLab CI/CD について + +GitLab CI/CD では、Docker executor を用いて任意の Docker image 上でスクリプトを走らせることができる。 + +例: + +```yaml +hello-world: + stage: test + image: alpine:latest + script: + - 'echo "Hello, World!"' + rules: + - if: '$CI_MERGE_REQUEST_IID' + when: always +``` + +ここで、`script` に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。 + +では、次のようなケースだとどうなるか。 + +```yaml +hello-world: + stage: test + image: alpine:latest + script: + - 'exit 1 | exit 0' + rules: + - if: '$CI_MERGE_REQUEST_IID' + when: always +``` + +失敗するコマンドをパイプに接続した。通常 Bash では、パイプの最後のコマンドの exit code が全体の exit code になる。 + +{#pipefail-option} +## `pipefail` オプションについて + +前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、`pipefail` オプションを有効にする。 + +```bash +# On にする +set -o pipefail +# Off にする +set +o pipefail +``` + +こうすると、パイプ全体が失敗するようになる。 +この設定は、デフォルトだと off になっている。 + +{#problem} +# 発生した問題 + +次のような GitLab CI/CD ジョブが失敗してしまった。 + +```yaml +hoge: + stage: test + image: alpine:latest + script: + - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"' + rules: + - if: '$CI_MERGE_REQUEST_IID' + when: always +``` + +`grep` コマンドは、パターンにマッチする行が一行もなかったとき、exit code 1 を返す。よって、`pipefail` が on になっていると、このジョブは失敗する。 +現在の `pipefail` がどうなっているか確かめるため `set +o` で全オプションを出力させたところ、`pipefail` が on になっていた。 + +しかし、先述したように Bash における `pipefail` のデフォルト値は off のはずだ。 +実際に、ローカルで `alpine:latest` を動かしてみたところ、 + +``` +$ docker run --rm alpine:latest sh -c "set +o" +set +o errexit +set +o noglob +set +o ignoreeof +set +o monitor +set +o noexec +set +o xtrace +set +o verbose +set +o noclobber +set +o allexport +set +o notify +set +o nounset +set +o vi +set +o pipefail +``` + +確かに `pipefail` は無効になっている。 + +なぜスクリプト内で `set -o pipefail` しているわけでもないのに `pipefail` が on になっているのか。 + +{#where-pipefail-is-enabled} +# どこで `pipefail` が on になるか + +`.gitlab-ci.yml` で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。 +そう仮説を立てて [GitLab Runner のリポジトリ](https://gitlab.com/gitlab-org/gitlab-runner) を調査したところ、 [ソースコード中の以下の箇所](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/c75da0796a0e3048991dccfdf2784e3d931beda4/shells/bash.go#L276) で `set -o pipefail` していることが判明した (コメントは筆者による)。 + +```go +// pipefail オプションが存在しない環境にも対応するため、 +// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している +buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n") +``` + +{#how-to-solve} +# どのように解決するか + +通常の Bash スクリプトを書く場合と同様に、`pipefail` が on になっていては困る場所だけ off にしてやればよい。 + +```yaml + hoge: + stage: test + image: alpine:latest + script: ++ - 'set +o pipefail' + - 'cat hoge.txt | grep piyo | sed -e "s/foo/bar/g"' ++ - 'set -o pipefail' # この例の場合、ここで終わりなので戻さなくてもよい + rules: + - if: '$CI_MERGE_REQUEST_IID' + when: always +``` + +{#remarks} +# 備考 + +なお、上述した実装ファイルは `shells/bash.go` だが、`alpine:latest` の例でもそうであったように、シェルが `sh` である場合にも適用される。 diff --git a/services/blog/content/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands.dj b/services/blog/content/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands.dj new file mode 100644 index 00000000..5738de84 --- /dev/null +++ b/services/blog/content/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands.dj @@ -0,0 +1,86 @@ +--- +[article] +uuid = "9b26c1ed-45c3-4cad-9476-cbf2cf2e4de7" +title = "【Zsh】 Composer のカスタムコマンドに対する Zsh 補完で引数にファイルを補完させる" +description = "Zsh の Composer に対する補完はカスタムコマンドやその引数を補完しない。カスタムコマンドの引数としてファイルを補完させる方法を調べた。" +tags = [ + "composer", + "php", + "zsh", +] + +[[article.revisions]] +date = "2024-04-29" +remark = "公開" +--- +{#version-info} +# バージョン情報 + +* Composer: 2.7.4 +* PHP: 8.3.6 +* Zsh: 5.9 + +{#intro} +# はじめに + +[Composer](https://getcomposer.org/) は PHP のデファクトスタンダードなパッケージマネージャである。 +Zsh では、`composer` コマンドに対する補完が提供されており、`composer` と入力してタブキーを押すと、利用可能なコマンドやオプションが補完される。 +Zsh の補完はシェル関数の形で実装されており、`composer` コマンドに対応した補完をおこなうのは `_composer` である。 +[記事執筆時点での補完関数の定義は、GitHub のミラーリポジトリから参照できる。](https://github.com/zsh-users/zsh/blob/a66e92918568881af110a3e2e3018b317c054e4a/Completion/Unix/Command/_composer) + +{#problem} +# 発生していた問題 + +`composer` コマンドはカスタムコマンド (`composer.json` の `scripts` で定義されたコマンド) に対して補完をおこなわない。 +つまり、途中まで入力されたカスタムコマンドを補完しないし、カスタムコマンドの引数も補完しない。 +例えば、PHPUnit を呼び出す `phpunit` というカスタムコマンドを定義し `composer phpu` まで打ってタブキーを押しても、`composer phpunit` にはならない。 +また、`composer phpunit -- --` まで打ってタブキーを押しても、`phpunit` コマンドのオプションは補完されない。 + +このことは、先ほどリンクを載せた `_composer` 関数を定義しているファイルの冒頭にも書かれている。 + +```zsh +# - @todo We don't complete custom commands (including script aliases). This is +# easy to do in the general case, but it probably requires some clever caching +# to avoid introducing a noticeable lag to every completion operation, due to +# the way command resolution works and the fact that discovering custom +# commands requires making slow calls to Composer +``` + +{#what-i-want-to-achive} +# やりたいこと + +確かに、カスタムコマンドに対して完全な補完を提供するのは不可能か、あるいは実現できても遅くなりすぎるだろう。 +しかし、不完全なフォールバックを提供するくらいなら可能なはずだ。 + +この記事では、これらのカスタムコマンドについて、Zsh が提供するデフォルトのファイル・ディレクトリ補完を適用する。 +つまり、`composer phpunit -- tests/` まで打ってタブキーを押すと、`tests` ディレクトリの下にあるテストファイルまたはディレクトリが補完される。 + +{#solution} +# 解決策 + +まずは、Zsh で補完関数を提供する場合のボイラープレートコードを書く。 +以下は `~/.zshrc` にすべて書く前提だが、`autoload` を設定するなどすれば別ファイルに分離できる (詳細な手順は割愛)。 + +```zsh +compdef _my_composer composer composer.phar +``` + +`compdef` は Zsh が用意している関数で、第一引数に補完関数の名前、第二引数以降に補完を適用するコマンド名を並べる。 +この場合は、`composer` コマンドや `composer.phar` コマンドに対して `_my_composer` を使って補完をおこなうよう定義している。 + +次に `_my_composer` を定義する。基本的にはデフォルトの `composer` コマンドの補完関数 (つまり `_composer` 関数) を使い、それが何も返さなかった場合に限り、Zsh のファイル・ディレクトリ補完へフォールバックする。 + +```zsh +function _my_composer() { + _composer "$@" || _files "$@" +} +``` + +`_composer` コマンドは何も補完候補がなかったとき非ゼロな exit status で終了するので、そうであったなら `_files` を呼び出す。 +`_files` は、Zsh がデフォルトで用意しているファイル・ディレクトリの補完をおこなう関数である。 + +{#conclusion} +# まとめ + +これらの設定をおこなうことで、部分的ながら Composer のカスタムコマンドに対して補完をおこなうことができる。 +特に、PHPUnit や PHPStan などの対象ファイル・ディレクトリを引数に取るようなコマンドを使う場合に有用であろう。 diff --git a/services/blog/content/posts/2024-05-11/phpconkagawa-2024-report.dj b/services/blog/content/posts/2024-05-11/phpconkagawa-2024-report.dj new file mode 100644 index 00000000..a1ec6829 --- /dev/null +++ b/services/blog/content/posts/2024-05-11/phpconkagawa-2024-report.dj @@ -0,0 +1,64 @@ +--- +[article] +uuid = "f13aa9d6-4533-4a15-872a-c298ab2090db" +title = "PHP カンファレンス香川 2024 参加レポ" +description = "2024-05-11 に開催された、PHP カンファレンス香川 2024 に参加した。" +tags = [ + "conference", + "php", + "phpconkagawa", +] + +[[article.revisions]] +date = "2024-05-11" +remark = "公開" +--- +{#intro} +# はじめに + +2024-05-11 に開催された [PHP カンファレンス香川 2024](https://phpcon.kagawa.jp/2024/) に参加した。 + +{#session-thoughts} +# セッション感想 + +* 泥まみれの技術革新: あなたの[ PHPバージョンアップ | 新フレームワーク採用 | アーキテクチャ刷新 | … ]を後押しするために by nrslib + + * fortee URL: https://fortee.jp/phpconkagawa-2024/proposal/7f4622af-03b6-4b83-a0ef-e1cfc7b7c930 + * 感想: ちょうどとあるマイグレーション作業をしているので、頷きながら拝聴しました。結局は誰しも移行作業は根気と腕力なのだということに勇気をもらえました。 + +* PHP 9 に備えよ - 動的プロパティ、どうすればいぃ? by 荒瀬 泰輔 + + * fortee URL: https://fortee.jp/phpconkagawa-2024/proposal/039ebb21-d104-4df2-86bb-be2680979b7b + * 感想: これも上と同じく移行作業の話ではあり、結局のところは「頑張って地道にやっていく」しかないところもあります (とはいえこちらは静的解析である程度潰せますが)。PHP 言語のコミュニティ全体で頑張っていきましょう。 + +* 1人プロ・ペアプロ・モブプロの効果的な使い分け by まきまき + + * fortee URL: https://fortee.jp/phpconkagawa-2024/proposal/db3e9634-4a79-46c1-84fd-8ffa4d495a13 + * 感想: 今会社でペアプロを部分的に取り入れているものの、迷うところが多く、楽しみにしていた発表です。まずは何か一つ変えないことには始まらないので、発表から得たヒントを自分たちのチームに反映すべく、何かやりかたを変えてみる予定です。 + +* mb_trim関数を作りました - PHPに新しい関数を追加しました - by てきめん + + * fortee URL: https://fortee.jp/phpconkagawa-2024/proposal/0ec36f50-c4b7-4aa4-abef-006f8bab3931 + * 感想: RFC を必要とするような機能追加のプロセスを日本語で解説する資料がどんどんと増えていくのは、ハードルを下げるという意味で非常にありがたいです。私も以前から出そう出そうと考えている書きかけの RFC があるのですが、具体的なプロセスが明示されるとやはりやる気になりますね。 + +* (「PHPカンファレンス小田原2024」を実行委員長がふりかえる by asumikam) + + * fortee URL: https://fortee.jp/phpconkagawa-2024/proposal/c1efd828-72c9-4719-93f7-2ca3f8f20ac1 + * 備考: ちょっとしたトラブルにより午前中の発表が見られなかったので、生で拝聴したわけではなく、スライドを拝見して感想を書いています。 + * 感想: Thanks のスライド非常に嬉しかったです。こちらこそ素晴らしいカンファレンスの場をありがとうございました!スタッフ募集あれば来年も是非参加させてください。 + +{#lightning-talk} +# 懇親会 LT + +今回登壇者ではなかったのだが、プロポーザル募集時に用意していたスライド (LT 用に作っていたのだが、そもそも LT 枠がなかったのでお蔵入りになっていた) があったので懇親会の LT で発表した。 + +中身は [第150回PHP勉強会@東京で登壇した内容](/slides/2023-03-15/phpstudy-tokyo-150/) とほぼ同じで、タイトルを「うどんのように細長い FizzBuzz を書く」にしただけの手抜き・一発ネタ発表である。個別にスライドはアップロードしないので、前述のリンクを参照してほしい。 + +なお、この発表には [ブログ記事バージョン](/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line/) もある。 + +{#outro} +# おわりに + +午前中の発表に間に合わなかったことがとにかく心残りなのだが、それ以外は PHP カンファレンス小田原のスタッフの方々をはじめ多くの方と交流でき、非常に楽しいカンファレンスだった。来年もあるそうなので (この分だと来年も月刊 PHP カンファレンスにならないか?)、是非参加したい。 + +あれ、そういえば香川でうどん食べてないな......。 diff --git a/services/blog/content/posts/2024-06-19/scalamatsuri-2024-report.dj b/services/blog/content/posts/2024-06-19/scalamatsuri-2024-report.dj new file mode 100644 index 00000000..85d713de --- /dev/null +++ b/services/blog/content/posts/2024-06-19/scalamatsuri-2024-report.dj @@ -0,0 +1,48 @@ +--- +[article] +uuid = "8d6f3690-3da3-4235-a81b-b9707cee22ad" +title = "ScalaMatsuri 2024 参加レポ" +description = "2024-06-08 から 2024-06-09 にかけて開催された、ScalaMatsuri 2024 に参加した。" +tags = [ + "conference", + "scala", + "scalamatsuri", +] + +[[article.revisions]] +date = "2024-06-19" +remark = "公開" +--- +{#intro} +# はじめに + +2024-06-08 から 2024-06-09 にかけて開催された [ScalaMatsuri 2024](https://2024.scalamatsuri.org/ja) に参加した。 + +Day 2 には当日参加できなかったため、day 2 のセッションの感想は YouTube にアップロードされたアーカイブ動画を観て書いている。 + +{#sessions} +# セッション感想 + +特に印象に残ったセッションを、day 1 と day 2 で一つずつ選んだ (タイトルと登壇者名は [公式ホームページの「プログラム」](https://2024.scalamatsuri.org/ja/programs) から引用)。 + +* [Scala to WebAssembly: 動機と方法](https://2024.scalamatsuri.org/ja/programs/SESSION_DAY_1_02) (Rikito Taniguchi さん) + + * [最近 WebAssembly の処理系を作った](/posts/2024-03-17/phperkaigi-2024-report/#section--as-speaker) こともあって、気になっていたセッションです。私の処理系は WasmGC proposal を実装していないので動かせないのですが、いつかサポートして動かしてみたいですね。 + +* [作って学ぶ Extensible Effects](https://2024.scalamatsuri.org/ja/programs/SESSION_DAY_2_04) (Kory さん・hsjoihs さん) + + * 今回一番楽しみにしていたセッションです。Day 2 当日は参加できず、後日アーカイブ動画を視聴したのですが、期待を裏切らない濃厚なセッションでした。後日開かれた [NB-Scala レトロスペクティブ (非公式後夜祭)](https://nextbeat.connpass.com/event/315988/) の発表も拝聴したのですが、どちらも非常に面白かったです。 + +{#others} +# その他感想 + +* 良い会場だった。よく取り沙汰されるスライドの文字サイズの問題は、巨大なスクリーンを用意することで解決するという発見があった +* ランチにお弁当が用意されており、おいしかった ( [参考画像](https://x.com/nsfisis/status/1799276217583260092) ) + +{#outro} +# おわりに + +私が Scala を書いたり追ったりしていたのは Scala 2 の頃で、Scala 3 はほとんど浦島太郎状態だったのだが、非常に楽しく面白いイベントだった。 +イベントに触発されて、長らく塩漬けになっていた Scala 製の趣味プロジェクトを久しぶりに触っているのだが、これもまた楽しい。 + +ScalaMatsuri 運営の皆さま、スピーカーの皆さま、スポンサーの皆さま、最高のイベントをありがとうございました!次回も楽しみにしています。 diff --git a/services/blog/content/posts/2024-07-19/reparojson-fix-only-json-formatter.dj b/services/blog/content/posts/2024-07-19/reparojson-fix-only-json-formatter.dj new file mode 100644 index 00000000..eb63da08 --- /dev/null +++ b/services/blog/content/posts/2024-07-19/reparojson-fix-only-json-formatter.dj @@ -0,0 +1,132 @@ +--- +[article] +uuid = "222488dd-cf07-4961-83aa-a014b05369ff" +title = "reparojson: 文法エラーを直すだけの JSON フォーマッタを作った" +description = "文法エラーだけを直し、空白の削除や挿入といった整形処理を一切おこなわない JSON フォーマッタを作成した。Neovim と連携させる設定例も紹介する。" +tags = [ + "neovim", + "vim", +] + +[[article.revisions]] +date = "2024-07-19" +remark = "公開" +--- +::: note +この記事は [Vim 駅伝](https://vim-jp.org/ekiden/) #218 の記事です。 +::: + +{#intro} +# 欲しかったもの + +Vim で JSON を編集しているときに、文法エラー (末尾カンマやカンマの不足) のみを修正して一切の整形をおこなわないプラグインが欲しかった。 +整形も同時におこなうプラグインは見つかっただけでも多数あったのだが、整形しないものは見つけられなかったので自作することにした。 + +なお、作成したツール自体は単体の CLI として動作し、Vim とは無関係に使うことができる。 +この記事では Neovim と組み合わせる場合の設定を紹介するが、およそ任意のエディタで使えるだろう。 + +{#reparojson} +# 作ったもの + +作成したものがこちら: [ReparoJSON](https://github.com/nsfisis/reparojson) + +次のように動作する。 + +``` +$ echo '[ 1 2 ]' | reparojson +[ 1, 2 ] + +$ echo '[ 1, 2, ]' | reparojson +[ 1, 2 ] + +$ echo '{ "foo": 1 "bar": 2 }' | reparojson +{ "foo": 1, "bar": 2 } + +$ echo '{ "foo": 1, "bar": 2, }' | reparojson +{ "foo": 1, "bar": 2 } +``` + +バージョン 0.1.1 時点で修正対象の文法エラーは次のとおり: + +* 配列末尾の余計なカンマ (削除する) +* 配列内のカンマ不足 (挿入する) +* オブジェクト末尾の余計なカンマ (削除する) +* オブジェクト内のカンマ不足 (挿入する) + +他にも自動で直せそうなエラーはいくつか思いつくが (オブジェクトのキーがクォートされていない等)、私自身があまり困っていないので優先度は低い。 + +{#itegration-with-neovim} +# Neovim との連携 + +Neovim で JSON ファイルを保存したときに、上記のツールを自動で走らせるように設定する。 + +ここでは、 [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) と [efm-langserver](https://github.com/mattn/efm-langserver) を用いた設定例を紹介する。 + +```lua + local lspconfig = require('lspconfig') + + lspconfig.efm.setup({ + init_options = { documentFormatting = true }, + settings = { + rootMarkers = {".git/"}, + languages = { + json = { + { + formatCommand = "reparojson -q", + formatStdin = true, + }, + }, + }, + } + }) + + vim.api.nvim_create_autocmd('LspAttach', { + callback = function(e) + vim.api.nvim_create_autocmd('BufWritePre', { + buffer = e.buf, + callback = function() + vim.lsp.buf.format({ async = false }) + end + }) + end, + }) +``` + +ほとんどは nvim-lspconfig と efm-langserver を使う際のボイラープレートだが、`formatCommand` で `-q` フラグを指定していることに注意してほしい。 +このツールは、デフォルトでは JSON が修正された場合 exit code 1 で終了する。 +これは、入力が最初から正しかった場合と修正して正しくなった場合を区別するためだが、異常終了してしまうと置き換えが発生しない。 +そのため、`-q` フラグを指定して、修正されたときも exit code 0 で終了するようにしている。 + +{#outro} +# おわりに + +このツールが威力を発揮するのは、行の入れ換え時である。次のような JSON があり、 + +```json + { + "a": true, + "b": false + } +``` + +2行目と3行目を入れ換えて以下のように編集した。 + +```json + { + "b": false + "a": true, + } +``` + +これは不正な JSON だが、このツールを通せば次のようになる。 + +```json + { + "b": false, + "a": true + } +``` + +もちろん、このような操作を文法を壊さずにおこなう Vim プラグインは存在する。 +しかし、単なる行の入れ換えであれば `ddp` の3ストロークでおこなうことができ、専用のキーバインドを覚える必要もない。 +このツールを用いることで、より Vimmer-friendly な JSON 編集が可能となる。 diff --git a/services/blog/content/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range.dj b/services/blog/content/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range.dj new file mode 100644 index 00000000..6a6f9c3f --- /dev/null +++ b/services/blog/content/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range.dj @@ -0,0 +1,118 @@ +--- +[article] +uuid = "eed112e4-3227-4b3f-9991-7e11c288ee2b" +title = "【Go】 text/template の with や range の内側から外側の \".\" にアクセスする" +description = "Go言語の text/template における with や range は \".\" を上書きする。これらの内側から外側の \".\" にアクセスする方法を調べた。" +tags = [ + "go", +] + +[[article.revisions]] +date = "2024-08-19" +remark = "公開" +--- +{#tldr} +# TL;DR + +常にトップレベルを指す特殊変数 `$` を使えばよい。 + +{#intro} +# はじめに + +Go には、標準ライブラリにテンプレートライブラリ `text/template` がある。 +この `text/template` における制御構造、`with` と `range` は次のように使われる。 + +``` +# {{ .Title }} + +# User + +{{ with .User }} + {{ .Name }} ({{ .ID }}) +{{ end }} + +# Items + +{{ range .Items }} + - {{ . }} +{{ end }} +``` + +`text/template` の `.` は、現在の操作対象を表す特殊なオブジェクトである。 + +`with` や `range` は、`.` を変更する効果を持つ。 +`with` は引数に渡されたオブジェクトを `.` へセットして、内部のテンプレートを実行する。 +`range` は引数に渡されたイテレート可能なオブジェクトに対し、それぞれの要素を `.` へセットして、要素の個数だけ内部のテンプレートを実行する。 + +つまりこのテンプレートは、次のような構造をレンダリングしている (`Execute()` の第2引数)。 + +```go +tmpl.Execute(out, Params{ + Title: "foo", + User: User{ + ID: 123, + Name: "john", + }, + Items: []string{ + "hoge", + "piyo", + "fuga", + }, +}) +``` + +{#what-i-want-to-do} +# やりたいこと + +今回おこないたいのは、`with` や `range` の中で、その外側で使われていたトップレベルのオブジェクトを参照することだ。 + +``` +{{ with .User }} + ここから .Title を参照するには? +{{ end }} + +{{ range .Items }} + ここから .User を参照するには? +{{ end }} +``` + +`with` や `range` は、`.` を自身の対象オブジェクトに変更するので、 +単に `{{ with .User }}` の中で `.Title` と書いても、それは `User` の `Title` プロパティを参照しているとみなされる。 + +`text/template` では変数が使えるので、テンプレートの先頭で + +``` +{{ $params := . }} +``` + +とでもしておけば実現は可能である。 + +しかしながら、頻発するシチュエーションにしてはあまりに不恰好である。よりスマートな方法が用意されているはずだ。 + +{#solution} +# 解決方法 + +常にトップレベルを指す特殊変数 `$` を使えばよい。 + +``` +{{ with .User }} + {{ $.Title }} +{{ end }} + +{{ range .Items }} + {{ $.User.Name }} +{{ end }} +``` + +`$` は、テンプレートが実行されるときに渡されたオブジェクトを指す。 +これを使えば現在の `.` に関係なくトップレベルを参照できる。 + +このことは、[`text/template` の公式ドキュメント](https://pkg.go.dev/text/template#hdr-Variables)にも以下のように記載されている。 + +> When execution begins, $ is set to the data argument passed to Execute, that is, to the starting value of dot. + +{#reference} +# 参考 + +* [直接の出典である Stack Overflow の回答: "In a template how do you access an outer scope while inside of a "with" or "range" scope?"](https://stackoverflow.com/questions/14800204/in-a-template-how-do-you-access-an-outer-scope-while-inside-of-a-with-or-rang) +* [大元の出典である `text/template` の公式ドキュメント](https://pkg.go.dev/text/template#hdr-Variables) diff --git a/services/blog/content/posts/2024-09-28/mncore-challenge-1.dj b/services/blog/content/posts/2024-09-28/mncore-challenge-1.dj new file mode 100644 index 00000000..f862b55e --- /dev/null +++ b/services/blog/content/posts/2024-09-28/mncore-challenge-1.dj @@ -0,0 +1,37 @@ +--- +[article] +uuid = "ee7289ee-ff2e-439d-b343-7f87504192fd" +title = "MN-Core Challenge #1 参加レポ" +description = "2024-08-28 から 2024-09-24 にかけて開催された MN-Core Challenge #1 に参加した。" +tags = [ + "mncore-challenge", +] + +[[article.revisions]] +date = "2024-09-28" +remark = "公開" +--- +::: note +ただの参加記で解説はない。 +::: + +{#intro} +# はじめに + +2024-08-28 から 2024-09-24 の約1ヶ月に渡り開催された [MN-Core Challenge #1](https://mncore-challenge.preferred.jp/) に参加した。私 nsfisis ([あるいは `0b0100000111111000`](https://x.com/nsfisis/status/1838276770560364977)) はスコア 1181 で、最終順位 29 位だった。 + +この記事で解説はしないが、提出した回答はこちらのリポジトリ ([GitHub: nsfisis/mncore-challenge](https://github.com/nsfisis/mncore-challenge)) にアップロードしている。 + +{#thought} +# 感想 + +MN-Core には初めて触れたが、それでも問題なく全問 (除 FizzBuzz) 解けるよう線路が敷かれており、前半の問題を解くことで自然と後半を解くだけの知識が身に付くように設計されていた。 + +開催期間中はほぼ常に MN-Core Challenge のことを考え続けており、期間中 (前掲した回答を貯めるためのリポジトリを除き) 自分の Git リポジトリをほとんど触ることがなかった。途中更新ができずに苦しい時間もあったが、一つ気付くと一つ縮まる楽しいゴルフだった。 + +悔しいポイントも多数あるのだが、書いているとキリがないので自分で反省するだけにしておく。 + +{#outro} +# おわりに + +最後になりましたが、運営のみなさま、素晴しいコンテストをありがとうございました!非常に楽しい時間でした!第2回を首を長くして待っています! diff --git a/services/blog/content/posts/2024-12-04/cohackpp-report.dj b/services/blog/content/posts/2024-12-04/cohackpp-report.dj new file mode 100644 index 00000000..80da994f --- /dev/null +++ b/services/blog/content/posts/2024-12-04/cohackpp-report.dj @@ -0,0 +1,197 @@ +--- +[article] +uuid = "ea0593d3-691c-4e08-8db4-98b8925717ec" +title = "紅白ぺぱ合戦に参加<しました" +description = "2024-11-30 に開催された紅白ぺぱ合戦に参加し、ぺ陣営のメンバとして LT しました。" +tags = [ + "cohackpp", + "php", +] + +[[article.revisions]] +date = "2024-12-04" +remark = "公開" + +[[article.revisions]] +date = "2024-12-05" +remark = "「育てた」枠・「育てられた」枠を勘違いして逆に表記していたので修正" +--- +{#intro} +# はじめに + +2024-11-30 に開催された [紅白ぺぱ合戦](https://connpass.com/event/329428/) なる催しに参加しました。私は「ぺ」陣営のメンバとして LT をおこないました。 + +紅白ぺぱ合戦のイベントページにある説明を以下に引用します。 + +> Webエンジニアの [asumikam](https://x.com/asumikam) とWebエンジニアの [stefafafan](https://x.com/stefafafan) が2024年7月7日に結婚しました。 +> +> せっかくなので技術トークとかで紅白戦をしませんか?いいですね!やっていきます! +> +> 場所はァ!小田原ァ!盛り上がっていきましょゥ!!! + +ざっくりと言えば、テックカンファレンスの形式をとった結婚披露宴です。タイトルの「ぺ」は PHPer、「ぱ」は Perl Monger の略です。 + +{#thoughts} +# 感想 + +私は「ぺ」陣営のスピーカーとして LT をしていたのですが、その前にまずは登壇以外の感想を。 + +いや~最高でしたね。どの枠のスピーチの方も良かったのですが、特に (asumikam さん/stefafafan さんに)「育てられた」枠のお二方が印象に残っています。 +(asumikam さん/stefafafan さんを)「育てた」枠としてお世話になった方に声をかけることはできると思うんですよ。 +それだけでなく、「自分が育てたのだ」と言える人がいて、そしてそれに 100 点で応える人がいるということ。この素晴しさ。人徳。 + +改めて、asumikam さん、stefafafan さん、ご結婚おめでとうございます! + +{#lt} +# LT + +{#prepare} +## 合戦準備 + +さて、時を合戦の前に戻しまして、両陣営の登壇者が発表され徐々に謎のイベントの輪郭が見えてきた頃、asumikam さんから次のような連絡を受けました。 + + + +最初は直近のカンファレンスに出して落選したプロポーザルテーマを LT に編集して話そうとしていたのですが、この機会でなければ話せない・この機会で話すことに意味があるテーマにしようとネタ出しをおこない、最終的に次のテーマでの登壇となりました。 + +{#battle} +## いざ尋常に勝負 + +当日は、「プログラミングマナー講座」と題して発表をおこないました。 +結婚式のマナー、特に「忌み言葉」へフォーカスし、これを無理やりプログラミングに適用するというものです。 +[スライドはこちらにアップロードしています。](/slides/2024-11-30/cohackpp/) + +最終的にお祝いのメッセージを仕込んだソースコードで締めるという構成は、我ながら綺麗にまとまったと思っています。忌み言葉の案は他にも大量にあったのですが、技術 LT かつ結婚祝いスピーチにするためにどうしても最後のソースコードが必要だったので、時間の関係上それらには犠牲となってもらいました ( [ボツになった案のひとつ](https://x.com/nsfisis/status/1862798137452327206) )。 + +そもそも結婚式・披露宴でのスピーチ自体が初めてだったのでそれなりに緊張していたのですが、登壇時やその後の反応を伺う限り概ね好評だったようで良かったです。 + +{#congrats} +# ご結婚おめでとうございます + +https://github.com/nsfisis/cohackpp/blob/main/congrats.php + +```php +<?php +$s=<<<'Q' +<?php +% +$s=<<<'Q' +@$c=[`]; +$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","", +$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat +($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print( +str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim( +chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s)); +Q; +$c=['0AFOgQFOgQFOgQFOgQFOgQFEAgiBAUIECIEBQwQHgQE8AQYFBoEBOgQGBAaBAToEBwQFgQE6BQYFBIEB +OwQHBASBATwEBgUDgQE8BQYEA4EBPQQGBAOBAT0FBgEFgQERBhsIBAQMgQERKQQFC4EBESkFAQ6BAREp +FIEBESkUgQERKRSBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B +AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ4EPIEBDQY7gQENBjuBAQ0GO4EBDQU8gQENBTyBAQwG +PIEBDAY8gQEMBjyBAQwGPIEBDAY8gQEMBjyBAQwHO4EBDAc7gQENBzqBAQ0IOYEBDgg4gQEOCiUCD4EB +DwwdBw+BARAQDhEPgQERLg+BARItD4EBFCsPgQEXJRKBARsbGIEBToEBToEBToEBToEBToHQ', +'0AFOgQFOgQFOgQEPASMFFoEBDwMhBRaBAQ4FIAUWgQEOBSAFFoEBDQUhBRaBAQ0FIQUWgQEMBSIFFoEB +DAUiBRaBAQsFIwUWgQELBQgBGgUWgQEKBQkDGAUWgQEKBQgGBCoDgQEJBQkFBSoDgQEFAQMECQYFKgOB +AQQDAQUJBQYqA4EBBAgJBQcqA4EBAwkJBRkFFoEBBAcJBRoFFoEBBQYIBRsFFoEBBgYHBRsFFoEBBwYF +BRwFFoEBCAYDBR0FFoEBCQYCBR0FFoEBCgseBRaBAQsJHwUWgQEMByAFFoEBDAcFAxgFFoEBDQUFBBgF +FoEBDQQGBRYGFoEBDAUHBAcmBYEBCwUIBQYmBYEBCwQKBAYmBYEBCgQLBQUmBYEBCQUMBS+BAQgFDAYv +gQEDHC+BAQMdLoEBAx0ugQEDHi2BAQMJBAUIBC2BARAFCAQtgQEQBQgELYEBEAUJAS+BARAFESEHgQEQ +BREhB4EBBwEIBQYBCiEHgQEHBAUFBAQJIQeBAQcEBQUEBAkEGAUHgQEGBQUFBAUIBBgFB4EBBgUFBQUE +CAQYBQeBAQYFBQUFBAgEGAUHgQEGBAYFBQUHBBgFB4EBBgQGBQYEBwQYBQeBAQUFBgUGBQYEGAUHgQEF +BQYFBwQGBBgFB4EBBQQHBQcEBgQYBQeBAQUEBwUHBQUEGAUHgQEEBQcFBwUFBBgFB4EBBAUHBQgEBQQY +BQeBAQQECAUIBAUEGAUHgQEDBQgFCAEIBBgFB4EBAwUIBREEGAUHgQECBQkFEQQYBQeBAQIFCQURIQeB +AQQCCgURIQeBARAFESEHgQEQBREhB4EBEAURIQeBARAFEQQYBQeBARAFEQQYBQeBARAFEQQYBQeBARAF +EQQYBQeBAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQEOAjEBDIEBDgUqBwqBAQ0FJwwJgQENBSETCIEBDQURAQcXDIEBDQURGxCBAQ0FERcU +gQENBBIOBAUUgQEMBRIGDAUUgQEMBRIFDQUUgQEMBRIFDgQUgQEMBRIFDgQUgQEMBBMFDgQUgQELBRMF +DgQUgQELBRMFDgUTgQELBRMFDgUTgQEDGQcFDgUTgQEDGwUoA4EBAxsFKAOBAQMbBSgDgQEDGwUoA4EB +CgUKBQUFDwUSgQEKBAsEBgUQBBKBAQkFCwQGBRAFEYEBCQULBAYFEAURgQEJBQoFBgUQBRGBAQkFCgUG +BREFEIEBCQQLBQYFEQUQgQEIBQsFBgUSBQkBBYEBCAULBQYFEgUJAwOBAQgFCwUGBQoFBAUIBAKBAQgF +CwQHBQQLBAYHAwOBAQgECwUHFAUGBQQDgQEHBQsFAxgFBwQEA4EBBwULBQMVCQ4DgQEHBQsFAw8QDQOB +AQcEDAUDCRcLBIEBBgULBQUCHwgFgQEGBQsFKAQHgQEGBQsFM4EBBgULBTOBAQYFCgUKIgiBAQUHCQUK +IgiBAQUICAUKIgiBAQUKBgUKIgiBAQULBAULBRgFCIEBBA0DBQsFGAUIgQEEBQIHAgULBRgFCIEBBgMD +DAwFGAUIgQENCwwFGAUIgQEOCgwFGAUIgQEQBw0FGAUIgQERBwwFGAUIgQERCAsiCIEBEAoKIgiBARAL +CSIIgQEPDQgiCIEBDwUCBwcFGAUIgQEOBgMHBgUYBQiBAQ0GBQcFBRgFCIEBDAcGBgUFGAUIgQELBwgE +BgUYBQiBAQoHCgMGBRgFCIEBCQcMAQcFGAUIgQEIBxUFGAUIgQEHCBUiCIEBBggWIgiBAQQJFyIIgQEF +BxgiCIEBBQUaBRgFCIEBBgMbBRgFCIEBJAUYBQiBAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEZBi+BARkGL4EBGgUvgQEaBS+BARoFL4EBGgUvgQEaBS+BARoF +L4EBGgUvgQEaBRkBFYEBGgUZAxOBARoFDwEIBRKBARoFCwUIBxCBARoFBgoHCg6BARoVCQkNgQEJJgsJ +C4EBCSYMCQqBAQohEgkIgQEKGxoIB4EBChUhCQWBARkFIwkEgQEZBSUGBYEBGQUmBAaBARkFKAIGgQEZ +BTCBARkFMIEBGQUwgQEZBTCBARkFMIEBGQUwgQEZBTCBARkFCRAXgQEZBQQYFIEBGSMSgQEZJBGBARkS +CAwPgQEXDhIJDoEBFQwYCA2BARMMGwcNgQESDB0HDIEBEA4eBgyBAQ8IAwQfBguBAQ4IBAQfBguBAQ0H +BgQgBQuBAQwHBwQgBQuBAQsHCAUfBQuBAQoGCgUfBQuBAQoGCgUfBQuBAQkGCwUfBQuBAQkFDAUeBguB +AQgGDAUeBguBAQgGDAUdBwuBAQgGDAUdBgyBAQgGDAUcBwyBAQkFDAUbBw2BAQkGCwUZCQ2BAQkHCgUY +CQ6BAQoIBwYVCw+BAQsIBQcSDRCBAQwSChURgQENEQoTE4EBDhAKERWBARANDA4XgQESCwwLGoEBFQYO +BSCBAU6BAU6BAU6BAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEuBhqBAS4FG4EBLgUbgQEuBRuBARICGQYbgQEPBRkGG4EBDgYZ +BRyBAQ8FGQUcgQEPBhgFHIEBDwYXBhyBARAFFwUdgQEQBRcFHYEBEAYNERqBAREFChcXgQERBQccFYEB +EQYEIBOBARIFAg4DExGBARIRBwYEChCBARIOCgUHCQ+BARMKDQUJCA6BARIJDgYKCA2BAREIEAUNBwyB +ARAJEAUOBgyBAQ8KDwYPBguBAQ4MDgUQBwqBAQ4MDgURBgqBAQ0GAgYMBRMFCoEBDAYEBQwFEwYJgQEM +BgQFCwYTBgmBAQsGBQYKBRUFCYEBCgYHBQkGFQYIgQEKBQgGCAYVBgiBAQkGCQUIBRcFCIEBCQUKBgYG +FwUIgQEJBQsFBgUYBQiBAQgFDAYEBhgFCIEBCAUNBQMGGQUIgQEIBQ0GAgYZBQiBAQcFDwwaBQiBAQcF +DwwaBQiBAQcFEAobBQiBAQcFEAoaBgiBAQcFEQgbBgiBAQcFEgYcBgiBAQcFEQgbBQmBAQcFEAoZBgmB +AQcFEAoZBgmBAQcFDwwXBgqBAQcFDg4VBwqBAQcGDAcCBxQGC4EBCAULBwQEFQcLgQEIBggIBgIVBwyB +AQgIBAkHARUHDYEBCRMcCQ2BAQoRHAkOgQELDhwKD4EBDAwbChGBAQ4HGwwSgQEsDxOBASgRFYEBKQ4X +gQEpDBmBASoIHIEBKwMggQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQE1DwqBASkbCoEBHiYKgQETMQqBAQc9CoEBBjQU +gQEGIQQKGYEBBhcOBxyBAQcOFAcegQEHBhsHH4EBJwYhgQEmBiKBASUGFgEMgQEkBhUDDIEBIwYWBAuB +ASMGFwQKgQEiBg8DBgQKgQEhBg8EBwQJgQEhBhAEBwQIgQEgBhEFBgQIgQEgBRMEBwQHgQEfBhQEBgUG +gQEfBhQEBwQGgQEfBRYEBgIIgQEeBhYEEIEBHgYXBA+BAR4FGAMQgQEeBSuBAR0GK4EBHQYrgQEdBiuB +AR0GK4EBHQYrgQEdBiuBAR0GK4EBHQYrgQEdBiuBAR4FK4EBHgYqgQEeBiqBAR4HKYEBHwYpgQEfByiB +ASAHJ4EBIAcngQEhByaBASEJJIEBIgkjgQEjCiGBASQLH4EBJQwdgQEmDxmBASgTE4EBKhMRgQErEhGB +AS4PEYEBMAwSgQE0CBKBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXATaBARQENoEBEgY2gQESBzWBARMGNYEBEwc0gQEUBjSB +ARQGNIEBFQYzgQEVBiABEoEBFgYeAxGBARYGHAYQgQEWBxoHEIEBFwYYCg+BARcHFQsQgQEYBhMLEoEB +GAYRCxSBARkGDgsWgQEZBgwLGIEBGgYJCxqBARoHBwocgQEbBgUKHoEBGwcCCiCBARwRIYEBHA8jgQEd +DCWBAR0KJ4EBHAoogQEbCSqBARoILIEBGQgtgQEXCC+BARYIMIEBFQgxgQEVBzKBARQHM4EBEwc0gQES +BzWBARIGNoEBEQY3gQERBjeBAREFOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQBjiBARAGOIEBEAY4gQEQ +BjiBARAHN4EBEAc3gQERBzaBAREINYEBEgkzgQETCiEDDYEBEw0WCw2BARQtDYEBFisNgQEXKg2BARon +DYEBHSARgQEjDxyBAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQEXBDOBARcLLIEBFxQjgQEXIRaBARchFoEBGh4WgQEiFhaBASsN +FoEBNgEXgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEkDR2BAR4WGoEBGR0YgQEVIxaBARApFYEB +DRcLCxSBAQ4QFAkTgQEODBoIEoEBDgkeBxKBAQ4GIgcRgQEPAiYGEYEBNwcQgQE4BhCBATgGEIEBOAYQ +gQE4BhCBATkFEIEBOQUQgQE5BRCBATgGEIEBOAYQgQE4BhCBATgGEIEBNwcQgQE3BhGBATcGEYEBNgcR +gQE1BxKBATUHEoEBNAcTgQEzCBOBATIIFIEBMQgVgQEvCRaBAS4JF4EBLAoYgQEqCxmBAScMG4EBJQ0c +gQEhDx6BARwTH4EBGBQigQEZESSBARoNJ4EBGgoqgQEbBS6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFDAgmBAUEECYEBQgQIgQE7AQYFB4EBOQQGBAeBATkEBwQGgQE5BQYFBYEB +OgQHBAWBATsEBgUEgQE7BQYEBIEBPAQGBASBATwFBgEGgQEQBhsIBAQNgQEQKQQFDIEBECkFAQ+BARAp +FYEBECkVgQEQKRWBAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6B +AU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAU6BAQ0EPYEBDAY8gQEMBjyBAQwGPIEBDAU9gQEMBT2BAQsG +PYEBCwY9gQELBj2BAQsGPYEBCwY9gQELBj2BAQsHPIEBCwc8gQEMBzuBAQwIOoEBDQg5gQENCiUCEIEB +DgwdBxCBAQ8QDhEQgQEQLhCBAREtEIEBEysQgQEWJROBARobGYEBToEBToEBToEBToEBToHQ', +'0AFOgQFOgQFOgQFOgQFCAwmBAUEECYEBQQUIgQE5AwYFB4EBOAQHBAeBASkCDgQGBQaBASYGDQUGBAaB +ASYGDgQHBAWBASYGDgUGBAWBAScFDwQHBASBAScGDwQGAwWBAScGDwUNgQEoBRAEDYEBKAUQAw6BASgG +IIEBKQUQAw2BASkFDAcNgQEpBgYMDYEBCgMdFw2BAQsVAh8NgQELNQ6BAQsxEoEBCysYgQELJh2BARkI +CwUdgQEsBhyBAS0FHIEBLQUcgQEuBRuBAS4FG4EBLwUagQEvBRqBATAFGYEBMAYYgQExBRiBATEGF4EB +MgUXgQEyBhaBATMGFYEBNAUVgQE0BhSBATUGE4EBEAIWCgMHEoEBEAYSFRGBAQ8GExURgQEPBRQUEoEB +DgYaDROBAQ4GIwQTgQEOBTuBAQ0GO4EBDQY7gQENBTyBAQ0FPIEBDQU8gQENBTyBAQ0FPIEBDQU8gQEN +BjuBAQ0GO4EBDgY6gQEOBzmBAQ4IOIEBDwg3gQEQCh4BFYEBEQwVBxWBARInFYEBEyYVgQEVJBWBARgh +FYEBGxoZgQFOgQFOgQFOgQFOgdA=', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQEJBz6BAQkGP4EBCQY/gQEJBigDFIEB +CQYlBhSBAQkGJQcTgQEJBiYHEoEBCQYnBhKBAQkGJwcRgQEJBigGEYEBCQYoBxCBAQkGKQYQgQEJBioG +D4EBCQYqBg+BAQkGKgcOgQEJBisGDoEBCgUrBg6BAQoFLAYNgQEKBSwGDYEBCgUtBgyBAQoFLQYMgQEK +BS0GDIEBCgUuBguBAQoFLgYLgQEKBS4GC4EBCgYtBguBAQoGLgYKgQEKBi4GCoEBCgYuBgqBAQoGLwYJ +gQELBS8GCYEBCwUvBgmBAQsFLwYJgQELBhMBGgYJgQELBhMCGgYIgQELBhMDGQYIgQEMBRMEGAYIgQEM +BhEGFwYIgQEMBhEGFwYIgQEMBhAGGQUIgQENBg8GGQUIgQENBg8GGQUIgQENBg4GGgILgQEOBg0GJ4EB +DgcLBiiBAQ4HCgcogQEPBwgHKYEBEAcGCCmBARAKAQkqgQEREiuBARIRK4EBEhAsgQETDi2BARULLoEB +FwcwgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgQFOgdA=', +'0AFOgQFOgQFOgQFOgQFOgQFOgQFOgQElBiOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYF +I4EBJgUQBQ6BAQ4IEAUGDw6BAQ4yDoEBDjIOgQEOMg6BAQ4rFYEBGhIigQEmBSOBASYFI4EBJgUjgQEm +BSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBRQCDYEBDQMWBQwKDYEBDQ8JHA2BAQ4zDYEBDjMN +gQEOMg6BARAnF4EBJQYjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUjgQEmBSOBASYFI4EBJgUj +gQEmBSOBAR0EBQUjgQEXFCOBARQZIYEBEh4egQERIRyBARAJDBAZgQEPBxARF4EBDgYSExWBAQ4FEwYD +CxSBAQ4FEwYFCxKBAQ0FFAYHChGBAQ0FFAYJCg+BAQ0FFAYKCg6BAQ0GEwYMCQ2BAQ4FEwYNCQyBAQ4G +EQYQBg2BAQ4HDwcRBQ2BAQ8IDAgSAw6BAQ8bFAEPgQERGCWBARIWJoEBFBIogQEXDSqBAU6BAU6BAU6B +AU6BAU6BAU6B0A==', +'0AFOgQFOgQFOgQFOgQFOgQFOgQEpBh+BASkGH4EBKQYfgQEqBR+BASoFH4EBKgUfgQEqBR+BASoFH4EB +KgUfgQEqBR+BARkuB4EBBkEHgQEHQAeBAQdAB4EBB0AHgQEHDBcFH4EBKgUfgQEqBR+BASoFH4EBKgUf +gQEqBR+BASoFH4EBKgUfgQEgDx+BAR4RH4EBHBMfgQEbFB+BARoIBAkfgQEZBwkGH4EBGQYLBh6BARgG +DQUegQEYBQ4GHYEBFwYPBR2BARcFEAUdgQEXBRAFHYEBFwUQBhyBARcFEAYcgQEXBQ8HHIEBFwUPBxyB +ARcGDgccgQEXBg0IHIEBGAYMBx2BARgHCggdgQEZCAYKHYEBGhcdgQEbFh2BARwVHYEBHgsBBh6BASAG +BAYegQEpBh+BASkGH4EBKAYggQEnByCBASYHIYEBJQcigQEkCCKBASIJI4EBIAokgQEeCyWBARwLJ4EB +GQ0ogQEWDiqBARcMK4EBGAktgQEZBTCBARoCMoEBToEBToEBToEBToEBToEBToHQ',]; +$m="";for($k=0;$k<min(13,intdiv(__LINE__-119,80)+1);$k++){$C=str_replace("\n","", +$c[$k]);$f=!0;foreach(str_split(base64_decode($C))as$l){$L=ord($l);$m.=str_repeat +($f?"#":chr(32),$L&127);$f=!$f;if($L&128){$m.="\n";$f=!0;continue;}}}print( +str_replace([chr(96),chr(37),chr(64)],[implode("\n",array_map(fn($C)=>"'".trim( +chunk_split(str_replace("\n","",$C),80,"\n"))."',",$c)),"\n{$m}","{$s}\nQ;\n"],$s)); +``` diff --git a/services/blog/content/posts/2024-12-04/cohackpp-report/lt.png b/services/blog/content/posts/2024-12-04/cohackpp-report/lt.png Binary files differnew file mode 100644 index 00000000..1075d95f --- /dev/null +++ b/services/blog/content/posts/2024-12-04/cohackpp-report/lt.png diff --git a/services/blog/content/posts/2024-12-33/2024-reflections.dj b/services/blog/content/posts/2024-12-33/2024-reflections.dj new file mode 100644 index 00000000..88b6c9b9 --- /dev/null +++ b/services/blog/content/posts/2024-12-33/2024-reflections.dj @@ -0,0 +1,84 @@ +--- +[article] +uuid = "d7f98354-83fc-4cf1-8769-2784f0ebb6c8" +title = "2024年の振り返り" +description = "2024年にやったことを振り返る" +tags = [ +] + +[[article.revisions]] +date = "2025-01-02" +remark = "公開" +--- +{#intro} +# はじめに + +ご存じのとおり、4 と 11 と 23 で割り切れる年は閏年というやつで 12 月が 33 日まである。 +1年の振り返りを書く猶予が平年よりも長くなるので大変に都合がよい。 + +去年のやつ: [/posts/2023-12-31/2023-reflections/](/posts/2023-12-31/2023-reflections/) + +{#conference} +# 登壇・カンファレンス参加 + +参加または登壇した勉強会やカンファレンス。 +LT 等も含めて計 8 回の登壇をおこなった。 +また、4つのカンファレンスでコアスタッフまたは当日スタッフとして参加した。 + +* PHP カンファレンス北海道 2024 オンラインで参加 +* [PHP 勉強会@東京 第 160 回 登壇](/slides/2024-01-24/phpstudy-tokyo-160/) +* [YAPC::Hiroshima 2024 参加](/posts/2024-02-10/yapcjapan-2024-report/) +* [PHPカンファレンス関西 2024 参加](/posts/2024-02-22/phpkansai-2024-report/) +* PHPerKaigi 2024 + + * [登壇](/slides/2024-03-08/phperkaigi-2024/) + * コアスタッフとして参加 + +* [Ya8 2024 登壇](/slides/2024-03-15/ya8-2024/) +* PHP カンファレンス小田原 2024 + + * [登壇](/slides/2024-04-13/phpcon-odawara-2024/) + * 当日スタッフとして参加 + +* [PHP 勉強会@東京 第 163 回 LT で登壇](/slides/2024-04-25/phpstudy-tokyo-163/) +* [PHP カンファレンス香川 2024 参加](/posts/2024-05-11/phpconkagawa-2024-report/) +* [ScalaMatsuri 2024 参加](/posts/2024-06-19/scalamatsuri-2024-report/) +* [PHP 勉強会@東京 第 166 回 登壇](/slides/2024-07-18/phpstudy-tokyo-166/) +* iOSDC Japan 2024 コアスタッフとして参加 +* Nix meetup #1 参加 +* [PHP 勉強会@東京 第 169 回 登壇](/slides/2024-10-30/phpstudy-tokyo-169/) +* [紅白ぺぱ合戦 LT で登壇](/slides/2024-11-30/cohackpp/) +* PHP カンファレンス 2024 当日スタッフとして参加 + +{#articles} +# 書いた記事 + +今年はこのブログに月1記事以上の記事を書くという目標を立てていた。本数としては 12 本以上あるが、10月と11月はゼロになってしまった。 +社内記事を社外向けにリライトする作業を中々進められていないので、2025年は定期的に消化していきたい。 + +* 社外記事 (このブログ): 15本 +* 社内記事: 22本 + + * 年間で最も記事を書いた人として社内表彰された + +{#coding} +# 作ったもの + +今年は主に WebAssembly ランタイムと、カンファレンスの企画で使うシステムを作っていた。 +後者のシステムでもサンドボックス化のための技術として WebAssembly を用いているので、今年は WebAssembly と戯れた一年だったと言える。 + +* [Waddiwasi: pure PHP で書かれた WebAssembly ランタイム](https://github.com/nsfisis/php-waddiwasi) +* [Albatross.PHP: PHPerKaigi 2024 のコードゴルフ企画で使われたシステム](https://github.com/nsfisis/phperkaigi-2024-albatross) +* [Albatross.swift: iOSDC Japan 2024 のコードバトル企画で使われたシステム](https://github.com/nsfisis/iosdc-japan-2024-albatross) +* [ReparoJSON: 文法エラーを直すだけの JSON フォーマッタ](/posts/2024-07-19/reparojson-fix-only-json-formatter/) + +{#misc} +# その他 + +* [MN-Core Challenge #1 に参加](/posts/2024-09-28/mncore-challenge-1/) +* ISUCON 14 に参加 + +{#outro} +# おわりに + +今年も大変お世話になりました。よいお年を! diff --git a/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1.dj b/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1.dj new file mode 100644 index 00000000..c3a5eb49 --- /dev/null +++ b/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1.dj @@ -0,0 +1,350 @@ +--- +[article] +uuid = "ce8f20e8-c79f-48f8-982d-53edd4d20483" +title = "PHPerKaigi 2023 トークン問題解説 (1/5)" +description = "PHPerKaigi 2023 でデジタルサーカス株式会社から出題した問題を解説する。全5問中の第1問。" +tags = [ + "conference", + "php", + "phperkaigi", + "piet", +] + +[[article.revisions]] +date = "2025-01-08" +remark = "公開" + +[[article.revisions]] +date = "2025-01-11" +remark = "読みやすさのため一部の文言を調整" +--- +{#intro} +# はじめに + +::: note +これは PHPerKaigi 2023 の記事です。今は 2025 年ですが、PHPerKaigi 2023 の記事です。 +::: + +2023-03-23 から 2023-03-25 にかけて開催された [PHPerKaigi 2023](https://phperkaigi.jp/2023/) では、PHPer チャレンジという企画がおこなわれた。 +PHPer チャレンジとは、スポンサーのパンフレットやカンファレンス会場などから「#」記号で始まる文字列を集め、景品などを得るという企画である。 +この文字列は「PHPer トークン」と呼ばれている。弊社 [デジタルサーカス株式会社](https://www.dgcircus.com/) からは、トークン問題という形で、PHP に関する問題を解くと PHPer トークンが得られるようになっている問題を出題した。 + +[PHPerKaigi 2023 の参加レポ](/posts/2023-04-04/phperkaigi-2023-report/) でも書いたとおり、この年のトークン問題は「昨年の PHPerKaigi 2022 が終わった段階から作り始め、約半年かけて制作」された。 +PHPerKaigi 当日も [PHPer チャレンジ解説セッション](/slides/2023-03-25/phperkaigi-2023-tokens/) という形で解説の機会を頂いたのだが、せっかく時間をかけて作題したので記事の形でも残しておこうと思う。 + +この記事では、全5問ある中の第1問について解説する。他の問題については以下のリンクを参照のこと。 + +1. [第1問 (この記事)](/posts/2025-01-08/phperkaigi-2023-tokens-q1/) +1. 第2問 (TODO: 執筆中) +1. 第3問 (TODO: 執筆中) +1. 第4問 (TODO: 執筆中) +1. 第5問 (TODO: 執筆中) + +それぞれの問題はこちらの GitHub リポジトリ ( [nsfisis/PHPerKaigi2023-tokens](https://github.com/nsfisis/PHPerKaigi2023-tokens) ) からも閲覧できる。 + +{#quiz} +# Q1: An Art of Computer Programming + +第1問『An Art of Computer Programming』はこちら。 + + + +{#how-to-solve} +# 解き方 + +まずはトークンを得る方法を解説抜きで説明する。次のように実行する。 + +``` +$ echo "#iwillblog" | php Q1.png >/dev/null +``` + +無事に実行できていれば「#ModernPHPisStaticallyTypedLanguage」というトークンが得られる。 + +{#commentary} +# 解説 + +{#read-as-image} +## 画像として解釈する + +まずは素直に画像として見てみよう。 +全体は QR コードになっている。適当な QR コードリーダで読み込むと、次のようなテキストが表示されるはずだ。 + +``` +Guess password. $ echo "password" | php Q1.png >/dev/null +``` + +メッセージは、この画像の実行方法とこの問題でやるべきこと (パスワードの推測) を示している。 + +次に QR コードの中央部に目を向けると、小さな文字で「Password is one of the PHPer tokens.」と書かれているのがわかる。 +他の PHPer トークンの中から適切な1つを見つけだし、「パスワード」として渡すことで答えとなる PHPer トークンが得られるというわけだ。 + +{#password} +## パスワード + +不正なパスワードを使って実行してみると、次のようなエラーメッセージが表示される。 + +``` +$ echo "foo" | php Q1.png >/dev/null +401 Unauthorized +``` + +すでに [「解き方」の節](#section--how-to-solve) で示したように、パスワードである PHPer トークンは「#iwillblog」である。これを与えて実行すると正解のトークンが得られる。 + +このパスワードの選択にはとある事情がある。 +今回の問題の作問は前回の開催 (PHPerKaigi 2022) 直後からスタートしており、この時点では PHPerKaigi 2023 で登録される PHPer トークンにどのようなものがあるかはまったくわからない状態であった。 +作問作業を早期に終わらせるには、次回開催でも確実に使われるであろう定番のトークンを予測して選ぶ必要があったのだ。 +かくして、私が知る限り毎回登場しているトークンである「#iwillblog」に白羽の矢が立てられた。 + +なお、解いてくださった方の中には、先頭の「#」を入力せずに何度も試してしまい答えが得られずじまいになった方もいらっしゃるようだった。 +問題を置いていたリポジトリにヒントとしてパスワードのトークンが「i」で始まると書いていたのだが、これが意図せずミスリードになってしまった。 +これは私のミスである。 + +{#png-steganography} +## PNG ステガノグラフィ + +QR コードも言っているように、このファイルは PNG 画像であるにもかかわらず PHP で実行することができる。なぜこのようなことが可能なのか。 + +PNG 画像のフォーマットは、次のようになっている。 + +1. マジックナンバーなど +1. PNG ヘッダ (`IHDR` チャンク) +1. 実際の画像データ (`IDAT` チャンク) +1. PNG フッタ (`IEND` チャンク) + +PNG フッタの後ろにあるデータは、画像ビューアには解釈されず、画像の表示には影響を与えない。したがって、PNG フッタの後ろには任意のデータを埋め込むことができる。 + +さて、PHP には、PHP プログラムの始まりを示すための PHP タグ (`<?php` または `<?`) がある。 +CLI で実行する場合、PHP タグよりも前にあるデータは標準出力へそのまま出力される。 + +この画像ファイルは次のような構造になっていた。 + +1. マジックナンバーなど +1. PNG ヘッダ (`IHDR` チャンク) +1. 実際の画像データ (`IDAT` チャンク) +1. PNG フッタ (`IEND` チャンク) +1. *PHP タグ (`<?php`)* +1. *通常の PHP ソースコード* + +PNG ファイルとして読むときは PNG フッタ以降は無視され、PHP スクリプトとして読むときは PHP タグ以前が無視されるという仕掛けである。 + +`strings` コマンドを使うと、隠されたデータを簡単に閲覧できる。 + +``` +IHDR +-HHc +<PLTE +IDATx +IEND +<?php +error_reporting(-1); +$b = unpack('C*', file_get_contents(__FILE__)); +$w = $b[20]+2; +$h = $b[24]+2; +// (以下略) +``` + +`IHDR` や `IEND` が PNG 画像の一部で、`<?php` からが実際のプログラムになっている。 +もちろんこれを PHP プログラムとして動かすと、PHP タグより前にある PNG 画像としてのデータはそのまま標準出力へと出力されてしまう。 +それを防ぐため、QR コードを読み込んだときの実行方法 + +``` +Guess password. $ echo "password" | php Q1.png >/dev/null +``` + +には標準出力を捨てるよう `>/dev/null` と指定されている。 + +なお、このように PNG 画像などに本来のデータとは異なる別のデータを隠すことを「ステガノグラフィ」( [Wikipedia「ステガノグラフィー」](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%86%E3%82%AC%E3%83%8E%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%BC) ) と呼ぶ。 + +{#php-program} +## 実行される PHP プログラム + +画像の正体がわかったところで、画像に隠されていた PHP プログラムについて見ていこう。 +先ほどは一部しか記載しなかったので、全体を載せる。 +なお、ある程度ゴルフしながら書いたので、空白こそ残しているものの可読性は非常に低いことと思う。 + +```php +<?php +error_reporting(-1); +$b = unpack('C*', file_get_contents(__FILE__)); +$w = $b[20]+2; +$h = $b[24]+2; +$cs = []; +for ($y = 0; $y < $h; $y++) + for ($x = 0; $x < $w; $x++) + $cs[$y*$w + $x] = ($x*$y === 0 || $x === $w-1 || $y === $h-1) + ? 0 + : $b[122+($y-1)*($w-1)+$x-1]; +$i = stream_isatty(STDIN) + ? [] + : array_map(ord(...), str_split(trim((string) fgets(STDIN)))); +$m = []; +$pc = 1*$w+1; +$dp = 0; +$cc = 1; +$c0 = 1; +$b = 0; +$ns = 0; +$o = ''; +while (true) { + $ns++; + if ($ns > 1e5) { + echo "infinite loop detected\n"; + break; + $c1 = $cs[$pc]; + $y = (6 + intdiv($c1-2, 3) - intdiv($c0-2, 3)) % 6; + $x = (3 + $c1%3 - $c0%3) % 3; + match (($c0 !== 1) * ($c1 !== 1) * ($y*3 + $x)) { + 1 => $m[] = $b, + 2 => array_pop($m), + 3 => $m[] = array_pop($m) + array_pop($m), + 4 => $m[] = (fn($x, $y) => $y - $x)(array_pop($m), array_pop($m)), + 5 => $m[] = array_pop($m) * array_pop($m), + 8 => $m[] = array_pop($m) === 0 ? 1 : 0, + 11 => $cc *= pow(-1, array_pop($m)), + 12 => $m[] = $m[count($m)-1], + 13 => $m = (fn($n, $d, $m, $l) => [ + ...array_slice($m, 0, $l-$d), + ...array_reverse([ + ...array_reverse(array_slice($m, $l-$d, $d-$n)), + ...array_reverse(array_slice($m, $l-$n)), + ]), + ])(array_pop($m), array_pop($m), $m, count($m)), + 15 => !empty($i) and $m[] = array_shift($i), + 16 => $o .= sprintf('%d', array_pop($m)), + 17 => $o .= sprintf('%c', array_pop($m)), + default => 'nop', + }; + $c0 = $c1; + for ($j = 0; $j < 8; $j++) { + $v = []; + if ($c1 === 1) { + $x = $pc % $w; + $y = intdiv($pc, $w); + $e = [($y+1)*$w-1, ($h-1)*$w+$x, $y*$w, $x][$dp]; + $z = [1, $w, -1, -$w][$dp]; + for ($ep = $pc; $ep !== $e; $ep += $z) + if ($cs[$ep] !== 1) break; + $ep -= $z; + $pc = $ep; + } else { + $q = [$pc]; + $ep = $pc; + while (!empty($q)) { + $qq = array_pop($q); + $v[$qq] = true; + foreach ([$qq+1, $qq+$w, $qq-1, $qq-$w] as $qp) { + if ($cs[$qp] !== $c1) continue; + if (isset($v[$qp])) continue; + $q[] = $qp; + $qx = $qp % $w; + $qy = intdiv($qp, $w); + $x = $ep % $w; + $y = intdiv($ep, $w); + if ( + ($dp === 0 && ($x < $qx || ($x === $qx && ($y<=>$qy) === $cc))) + || ($dp === 1 && ($y < $qy || ($y === $qy && ($qx<=>$x) === $cc))) + || ($dp === 2 && ($qx < $x || ($qx === $x && ($qy<=>$y) === $cc))) + || ($dp === 3 && ($qy < $y || ($qy === $y && ($x<=>$qx) === $cc))) + ) + $ep = $qp; + } + } + } + $np = $ep + [1, $w, -1, -$w][$dp]; + if ($cs[$np] !== 0) { + $b = count(array_keys($v)); + $pc = $np; + break; + } + if ($j === 7) break 2; + if ($j % 2 === 0) $cc = -$cc; + if ($j % 2 === 1) $dp = ($dp+1) % 4; +// The original Piet image is wrong: it outputs 403 error for invalid passwords. +// Failure of authentication should be notified by 401, not 403. +// I noticed that one month before PHPerKaigi, but I could not read or write (paint) +// Piet any longer at that time. +fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o)); +``` + +これは一体なんなのか。ずばり、難解プログラミング言語の一つ Piet のインタプリタである。 +Piet はピエト・モンドリアン (『赤・青・黄のコンポジション』などで知られる抽象画家) の作品にインスピレーションを受けて作られた、画像をソースコードとするプログラミング言語である。 +インタプリタは画像の各ピクセルの上を進みながら、色等に応じて特定の処理をおこなっていく。 +ここでは詳しい言語仕様については解説しないので、気になる方は [Wikipedia の記事「Piet」](https://ja.wikipedia.org/wiki/Piet) などを参照してほしい。 + +プログラムの冒頭にあるこの箇所 + +```php +$b = unpack('C*', file_get_contents(__FILE__)); +``` + +で `__FILE__` つまりこの画像ファイルを読み込んでいる。 +先ほど Piet は画像をソースコードにしていると説明した。 +そう、今回の問題の画像ファイル `Q1.png` は、PHP 製 Piet インタプリタであると同時に、Piet のソースコード画像でもあるのだ。 +QR コード中央のカラフルな部分が Piet の命令になっている。 + +{#piet-source-code} +## Piet のソースコード + +さて、Piet でどのようなコードが書かれて (いや、描かれて) いるのかを解説したいところだが、今の私にはできそうにない。 +というのも、すでに述べたように Piet は「難解プログラミング言語」である。 +およそ人が描いたり読んだりするようには作られていない。性質としては、パズルに近い代物である。 + +というわけで、ここではあらましを説明するだけでご容赦いただきたい。 +それぞれの部分はおおよそ次のようなことをやっている (再検証・再読解はしていないので大嘘かもしれない)。 + +* 左上: 入力受け付け + + * 標準入力から1文字ずつ読み込み、入力がなくなるまでスタックに積む。多分。 + +* 上辺、右辺: パスワードの検証 + + * 入力がパスワードと一致するか (= `#iwillblog` かどうか) を調べる。多分。 + +* 下辺、左辺、上辺の3列目、右辺の3列目、下辺の2列目: トークンの出力 + + * パスワードと一致していればここに飛んでくる。正解のトークンを出力する。多分。 + +* 右辺の2列目、上辺の2列目: 不正解のメッセージ出力 + + * パスワードと一致していなければここに飛んでくる。不正解のときのメッセージを出力する。多分。 + +ところで、先ほど掲載した Piet のインタプリタのソースコード末尾には次のような箇所がある。 + +```php +// The original Piet image is wrong: it outputs 403 error for invalid passwords. +// Failure of authentication should be notified by 401, not 403. +// I noticed that one month before PHPerKaigi, but I could not read or write (paint) +// Piet any longer at that time. +fwrite(STDERR, str_replace('403 Forbidden', '401 Unauthorized', $o)); +``` + +コメントにも書かれているが、この Piet のソースコード画像には誤りがあった。 +本来 HTTP のステータスコードを真似るのなら、認証の失敗には 401 を返さなければならない。 +しかし、Piet のソースは 403 を返すように書いてしまっていた。 +そのことに私が気付いたのは PHPerKaigi 2023 が開催されるひと月前で、その時点で私はこの Piet のソースコードを (ちょうどこの記事でそうなっているのと同じように) 読解できなくなっていた。 +さらに悪いことに、正しいメッセージ「401 Unauthorized」は元の「403 Forbidden」よりも3文字長い。 +3文字出力が長くなるということは、それだけ Piet で塗るべきピクセルが増えることを意味する。 +もはや3文字追加で出力するだけの余白はこの画像に残されていなかった (と思う。腕ききの Piet プログラマならできるかもしれないので挑戦してみてほしい)。 + +これを解決するために私が選んだのは、インタプリタを改造し、本来のメッセージとは異なるメッセージを無理やり出力させて帳尻を合わせることだった。 +そういうわけでこの Piet インタプリタは完全な Piet インタプリタではなく、「403 Forbidden」というテキストを絶対に出力できない。 + +{#misc} +## その他小ネタ + +ここまでで問題の核心部分は説明し終えたので、ここからは残った小ネタを紹介しておく。 + +この問題のタイトル『An Art of Computer Programming』は、ドナルド・クヌースの『The Art of Computer Programming』をパロディしたものである。 + +この問題で得られるトークン「#ModernPHPisStaticallyTypedLanguage」は特に元ネタがあるわけではない。当然のような顔で嘘を主張したかったのでこうなった。 + +{#outro} +# おわりに + +この問題の自己評価はこちら。 +問題の出題順はおおよそ作成した順になっているのだが、そのせいで難易度高めの問題が1問目に配置されてしまった。 +これは反省点の一つである。 + +* 難しさ: ★★★★ +* お気に入り度: ★★ +* 鮮やかさ: ★★★★★★★ diff --git a/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1/Q1.png b/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1/Q1.png Binary files differnew file mode 100644 index 00000000..7f099d74 --- /dev/null +++ b/services/blog/content/posts/2025-01-08/phperkaigi-2023-tokens-q1/Q1.png diff --git a/services/blog/content/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2.dj b/services/blog/content/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2.dj new file mode 100644 index 00000000..44e8a4f6 --- /dev/null +++ b/services/blog/content/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2.dj @@ -0,0 +1,76 @@ +--- +[article] +uuid = "da2a0cec-74b3-4c5e-b2a2-47fe79ef49f9" +title = "【YAML】YAML 1.1 と YAML 1.2 の主な破壊的変更" +description = "データ記述言語 YAML におけるバージョン 1.1 と 1.2 の主な破壊的変更をまとめた。" +tags = [ + "yaml", +] + +[[article.revisions]] +date = "2021-06-30" +remark = "デジタルサーカス株式会社の社内記事として公開" +isInternal = true + +[[article.revisions]] +date = "2025-01-26" +remark = "ブログ記事として一般公開" +--- +::: note +この記事は、2021-06-30 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。 +::: + +{#intro} +# はじめに + +データ記述言語の一つ YAML には 1.0、1.1、1.2 のバージョンがある。 +これらのうち、1.1 と 1.2 の間には無視できない非互換の変更が多く、1.2 に対応していないライブラリもある (Ruby 同梱の `yaml` など)。 +この記事では、YAML 1.1 と YAML 1.2 の主な破壊的変更を紹介する (影響範囲が広いものを抜粋しており、すべての非互換を網羅してはいない)。 + +参照した仕様書はこちら: https://yaml.org/spec/1.2.2/ext/changes/ + +{#breaking-changes} +# 主な破壊的変更 + +{#boolean-literals} +### Boolean としてパースされるトークンが `true` / `false` とその亜種のみに + +この変更の影響が最も大きいと思われる。 +YAML 1.1 では、boolean 値のリテラルとして `true`、`false` のほか `yes`、`no`、`y`、`n`、`on`、`off`、それらの大文字バージョンなどが認められていた。 +YAML 1.2 では、`true` と `false`、それらの大文字バージョン (`True`、`TRUE`、`False`、`FALSE`) のみが boolean としてパースされるようになった。 + +{#octal-literals} +### 八進数リテラルには `0o` が必須に + +C 言語などでは、`0` から始まる数字の列を八進数としてパースする。 +YAML 1.1 もこれに準じていたが、1.2 からは `0o` のプレフィクスが必須となった ("o" は "octal" の "o")。 +プログラミング言語では、Python や Haskell、Swift、Rust などがこの記法を採用している。 + +{#merging} +### `<<` によるマージが不可能に + +YAML 1.1 では、`<<` という文字列をキーに指定することで、マップをマージすることができた。 + +```yaml +x: &base + a: 123 +# => { "x": { "a": 123 } } + +y: + <<: *base + b: 456 +# => { "y": { "a": 123, "b": 456 } } +``` + +1.2 からはこれができなくなる。 + +{#number-separator} +### 数字を `_` で区切るのが禁止に + +`1234567` を `1_234_567` と書けなくなった。 + +{#outro} +# おわりに + +全体的に、_There's more than one way to do it._ から _There should be one - and preferably only one - obvious way to do it._ へ移行しているように思われる。 +データ記述言語としては望ましい方向性ではないかと感じる。 diff --git a/services/blog/content/posts/2025-02-24/phpcon-nagoya-2025-report.dj b/services/blog/content/posts/2025-02-24/phpcon-nagoya-2025-report.dj new file mode 100644 index 00000000..35a9e270 --- /dev/null +++ b/services/blog/content/posts/2025-02-24/phpcon-nagoya-2025-report.dj @@ -0,0 +1,50 @@ +--- +[article] +uuid = "13174dc7-c1a3-465f-9ba6-14f0bc6f5961" +title = "PHP カンファレンス名古屋 2025 参加レポ" +description = "2025-02-22 に開催された、PHP カンファレンス名古屋 2025 に参加した。" +tags = [ + "conference", + "php", + "phpcon-nagoya", +] + +[[article.revisions]] +date = "2025-02-24" +remark = "公開" +--- +{#intro} +# はじめに + +2025-02-22 に開催された [PHP カンファレンス名古屋](https://phpcon.nagoya/2025/) に参加した。 + +{#sessions} +# セッション感想 + +特に印象に残ったセッションを二つピックアップした (タイトルと発表者名は fortee のプロポーザルページによる)。 + +* [PHPで印刷所に入稿できる名札データを作る by 長谷川智希 さん](https://fortee.jp/phpcon-nagoya-2025/proposal/26795bcc-78dd-431e-9538-7450779fa2cf) + + * PHPerKaigi や iOSDC の名札は品質が高いので、他の勉強会やカンファレンスでもついつい使ってしまうのですが、その裏側を覗くことができ面白かったです。カンファレンスの1セッションという形でなければ触れることのないような話が聴けるのはカンファレンスに参加する醍醐味の一つだと思います。 + +* [PHP 製 OSS のメモリ問題を辻斬りしていく by sji さん](https://fortee.jp/phpcon-nagoya-2025/proposal/d3ecbb68-318d-4b03-abfe-9ecccc6beb81) + + * 今回一番楽しみにしていた発表です。 [Reli](https://github.com/reliforp/reli-prof) は以前 [自作の WebAssembly 処理系を高速化するのに使ったのもあり](/slides/2024-03-15/ya8-2024/) その強力さについてはある程度知っていたつもりでしたが、実際に広く使われているライブラリでの調査過程を見ると唸るばかりです。これをすべて (FFI こそ使っているものの) pure PHP で実装しているとは俄に信じられません。 + +{#my-session} +# 登壇したセッション + +[「PHP 処理系の garbage collection を理解する 〜メモリはいつ解放されるのか〜」](https://fortee.jp/phpcon-nagoya-2025/proposal/24a2ec04-ca57-46f1-905c-52143a449eea) というタイトルで登壇もおこなった。タイトルどおり、PHP の garbage collection (GC) について扱った発表である。 + +技術的な内容としては [PHP のマニュアルの GC に関する記述](https://www.php.net/manual/ja/features.gc.php) を出ていないものの、PHP 処理系の内部的な用語を使わないようにしたり、本質的でない処理を省いたりして、理解のための前提条件を減らせたのではないかと思う。 + +ところで今回スライドのフォントサイズを大きくするために各スライドの見出し部分を消してみたのだが、結局ほとんどのスライドで見出しらしき文言が必要になったので、あまり効果はなかったかもしれない。 + +{#outro} +# おわりに + +今回もカンファレンスくらいでしか聴けないようなセッションがいくつも聴けてよかった。 +また、ちょうど連休だったのもあり名古屋も楽しむことができた。 + +運営のみなさま、お疲れさまでした&ありがとうございました。 +次は PHPerKaigi 2025 で会いましょう。 diff --git a/services/blog/content/posts/2025-03-27/zip-function-like-command-paste-command.dj b/services/blog/content/posts/2025-03-27/zip-function-like-command-paste-command.dj new file mode 100644 index 00000000..8c9417fa --- /dev/null +++ b/services/blog/content/posts/2025-03-27/zip-function-like-command-paste-command.dj @@ -0,0 +1,93 @@ +--- +[article] +uuid = "99111377-27e7-427b-9dc5-a23f621fa826" +title = "zip 関数のようなコマンド paste" +description = "zip 関数のような動きをする paste コマンドについてのメモ。" +tags = [ + "note-to-self", +] + +[[article.revisions]] +date = "2021-03-22" +remark = "デジタルサーカス株式会社の社内記事として公開" +isInternal = true + +[[article.revisions]] +date = "2025-03-27" +remark = "ブログ記事として一般公開" +--- +::: note +この記事は、2021-03-22 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。 +::: + +{#intro} +# 実現したい内容 + +次の2ファイル `a.txt` / `b.txt` から出力 `ab.txt` を得たい。 + +`a.txt` + +``` +a1 +a2 +a3 +``` + +`b.txt` + +``` +b1 +b2 +b3 +``` + +`ab.txt` + +``` +a1 +b1 +a2 +b2 +a3 +b3 +``` + +ちょうど Python や Haskell などにある `zip` 関数のような動きをさせたい。 + +{#paste-command} +# 実現方法 + +記事タイトルに書いたように、`paste` コマンドを使うと実現できる。 + +``` +$ paste -d '\ +' a.txt b.txt > ab.txt +``` + +`paste` コマンドは複数のファイルを引数に取り、それらを1行ずつ消費しながら `-d` で指定した文字で区切って出力する。 +`-d` は区切り文字の指定で、デフォルトだとタブ区切りになる。 + +ファイル名には `-` を指定でき、その場合は標準入力から読み込んで出力する。 +このとき `paste - -` のように複数回 `-` を指定すると、指定した回数の行ごとに連結することができる。 +例えば `ab.txt` だとこうなる。 + +``` +$ paste - - < ab.txt +a1 b1 +a2 b2 +a3 b3 +``` + +これは標準入力を使うとき特有の挙動で、単に同じファイル名を指定してもこうはならない。 + +``` +$ paste ab.txt ab.txt +a1 a1 +b1 b1 +a2 a2 +b2 b2 +a3 a3 +b3 b3 +``` + +ときどき便利。 diff --git a/services/blog/content/posts/2025-03-28/http-1-1-send-multiple-same-headers.dj b/services/blog/content/posts/2025-03-28/http-1-1-send-multiple-same-headers.dj new file mode 100644 index 00000000..687ddef6 --- /dev/null +++ b/services/blog/content/posts/2025-03-28/http-1-1-send-multiple-same-headers.dj @@ -0,0 +1,102 @@ +--- +[article] +uuid = "046e4412-bee8-4ffe-9876-6cbeaa0caf6b" +title = "【HTTP】HTTP/1.1 で同じヘッダを2回送るとどうなるか" +description = "HTTP/1.1 で同じヘッダを2回送ったときの挙動について仕様を読んでまとめた。" +tags = [ + "http", +] + +[[article.revisions]] +date = "2022-08-18" +remark = "デジタルサーカス株式会社の社内記事として公開" +isInternal = true + +[[article.revisions]] +date = "2025-03-28" +remark = "ブログ記事として一般公開" +--- +::: note +この記事は、2022-08-18 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。 +::: + +{#intro} +# はじめに + +HTTP version 1.1 で同じ名前のヘッダを2回送ると、どのように解釈されるのか。仕様を確認した。 + +今回読んだ仕様は RFC 7230 で、こちらのリンクから閲覧できる: https://datatracker.ietf.org/doc/html/rfc7230 + +その中でも、https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2 を主に引用する。 + +ところで、HTTP 周りの仕様を探すときはここから飛ぶと便利: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Resources_and_specifications> + +{#specification} +# 仕様 + +{#sender} +### 送信側 + +> A sender MUST NOT generate multiple header fields with the same field +> name in a message unless either the entire field value for that +> header field is defined as a comma-separated list [i.e., #(values)] +> or the header field is a well-known exception (as noted below). + +【日本語訳 (私が訳したもので、公式なものではない)】 +送信者は、同じ field name の header field を複数生成してはならない (MUST NOT)。 +ただし、header field の値がコンマ区切りのリストとして定義されているか、header field がよく知られた例外 (後述) である場合はその限りでない。 + +{#recipient} +### 受信側 + +> A recipient MAY combine multiple header fields with the same field +> name into one "field-name: field-value" pair, without changing the +> semantics of the message, by appending each subsequent field value to +> the combined field value in order, separated by a comma. The order +> in which header fields with the same field name are received is +> therefore significant to the interpretation of the combined field +> value; a proxy MUST NOT change the order of these field values when +> forwarding a message. + +【日本語訳 (私が訳したもので、公式なものではない)】 +受信者は、同じ field name を持つ複数の header field を、メッセージの意味を変えないようにしつつ同じ順序で追加して、単一のコンマで区切られた `"field-name: field-value"` のペアに結合してよい (MAY)。 +したがって、同じ field name を持つ header field がどのような順序で受信されたかは、結合された値の解釈に影響する。 +よって、プロキシは、メッセージを転送する際、header field の順序を変えてはならない (MUST NOT)。 + +{#exception} +### 例外ケース: Set-Cookie + +> Note: In practice, the "Set-Cookie" header field ([[RFC6265](https://datatracker.ietf.org/doc/html/rfc6265)]) often +> appears multiple times in a response message and does not use the +> list syntax, violating the above requirements on multiple header +> fields with the same name. Since it cannot be combined into a +> single field-value, recipients ought to handle "Set-Cookie" as a +> special case while processing header fields. (See Appendix A.2.3 +> of [Kri2001] for details.) + +【日本語訳 (私が訳したもので、公式なものではない)】 +注意: 実際には、`Set-Cookie` header field ([RFC6265](https://datatracker.ietf.org/doc/html/rfc6265)) は、しばしばレスポンスメッセージ中に複数回現れる。 +これはリストの構文を使っておらず、上述した同じ field name を持つ header field についての要件に違反している。 +この値は単一の値へ結合できないため、受信者は、header field を処理する際、`Set-Cookie` を特別扱いした方がよい。 + +おそらく、「送信側」のところで書かれている「よく知られた例外」の一つがこれだと思われる。 + +{#comma-separated-list} +### どの header field がコンマ区切りのリストなのか + +上記のように、同じ field name を持つ header field を複数回送れるかどうかは、その header field がコンマ区切りのリストとして定義されているかどうかで決まる。では、特定の header field がその条件を満たしているかどうか知りたいときは、何を見ればよいのか。 + +HTTP の仕様として定義されているような header field であれば、下記のリンクからそれぞれの定義を参照できる。 + +* https://datatracker.ietf.org/doc/html/rfc7231#section-5 +* https://datatracker.ietf.org/doc/html/rfc7231#section-7 + +そうでない場合 (たとえば `X-` から始まるもの等) は、MDN や各ベンダのドキュメントを探すことになるだろう。 + +{#outro} +# まとめ + +* 送信側: 基本的には複数回送れない。コンマ区切りのヘッダは例外 +* 受信側: 基本的には未規定。コンマ区切りのヘッダは複数回来たらその順に結合する +* プロキシ: 順序を変えてはならない +* `Set-Cookie` は例外ケース diff --git a/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award.dj b/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award.dj new file mode 100644 index 00000000..f51396f8 --- /dev/null +++ b/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award.dj @@ -0,0 +1,199 @@ +--- +[article] +uuid = "039b3dff-3b75-46b7-a731-9a3a0ff8e21f" +title = "RubyKaigi 2025 の TRICK で入賞した" +description = "RubyKaigi 2025 で開催された TRICK において、『最もRuby on Ruby賞』として審査員賞をいただいた。" +tags = [ + "conference", + "ruby", + "rubykaigi", + "trick", +] + +[[article.revisions]] +date = "2025-04-20" +remark = "公開" +--- +{#intro} +# はじめに + +2025-04-16 から 2025-04-18 にかけて開催された [RubyKaigi 2025](https://rubykaigi.org/2025/) に参加した (私が参加できたのは 1日目の 2025-04-16 のみ)。 + +地元松山での大規模なカンファレンスということでスケジュールに無理を言わせて 1日目だけでもと参加したのだが、そこで開催された [TRICK 2025](https://github.com/tric/trick2025) で審査員賞をいただいた。 + +この記事では、提出した作品の紹介と解説をおこなおうと思う。 + + +{#trick} +# TRICK とは + +TRICK とは RubyKaigi で不定期に開催されているコンテストで、Ruby で書かれた「変わった」コードを表彰する。早い話が [IOCCC](https://www.ioccc.org/) の Ruby 版である。 + +存在を知ってから次こそは出したいと思っていたところ、ちょうど RubyKaigi の地元開催と被ったのでこれ幸いとエントリーした。 + + +{#my-work} +# 作品紹介 + +今回頂いたのは審査員賞の一つ eto award (公式の賞の名前に合わせて敬称略) で、"Most Ruby-on-Ruby" Award (『最もRuby on Ruby賞』) として受賞した (IOCCC と同じく、それぞれの賞に個別の名前が付く)。 + +ソースコード等はこちら: https://github.com/tric/trick2025/tree/main/10-nsfisis + +今回の TRICK では `ruby.wasm` の使用が認められている。 + +> * *(NEW)* You can use [ruby.wasm](https://github.com/ruby/ruby.wasm). + +適当に HTTP サーバを立てて [`index.html`](https://github.com/tric/trick2025/blob/main/10-nsfisis/index.html) を開くと、次のように [`entry.rb`](https://github.com/tric/trick2025/blob/main/10-nsfisis/entry.rb) の内容が表示される。 + + + +自身のソースコードを出力するプログラム、いわゆる quine の亜種になっている。 + +しかし、このプログラムは単にソースコードをそのまま出力するのではなく、 + +* シンタックスハイライトして、 +* 英単語や記号に振り仮名を振って、 +* HTML で、 + +表示している。つまり、Ruby プログラムにルビを振った作品である。例えば、先頭の2行目の `require` は次のような HTML で構成されている。 + +```html +<ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby> +``` + +順に使ったテクニックを解説していく。 + +{#quine} +## Quine + +改めて quine について説明する。Quine とは、自身のソースコードを出力するようなプログラムである。Ruby では様々な方法で quine を書くことができるが、この作品で使っている基本形は以下のようなものである。 + +{numbered="true"} +```ruby +eval $s=<<'EOS' +print "eval $s=<<'EOS'\n" +print $s +print "EOS\n" +EOS +``` + +変数 `$s` に 2 行目、3 行目、4 行目が入っており、それに加えて 1 行目と 5 行目を出力すれば元のソースコードが得られる。実際には `$s` を加工してシンタックスハイライトや振り仮名を振ることになる。 + +{#syntax-highlight} +## シンタックスハイライト + +シンタックスハイライトは、トークナイズとトークン種別に応じた色付けの2段階からなる。 + +トークナイズには Ruby 3.4 からデフォルトのパーサになった [Prism](https://github.com/ruby/prism) を利用している。 +`Prism.lex()` を使うとトークナイズができるので、トークンに付いているソースコード位置の情報を使いつつ元のソースコードを復元する。 + +```ruby +y = 1 # 現在の行 +x = 0 # 現在の列 +Prism.lex($s).value[..-2].each {|t, *| + l = t.location + r = l.start_line # トークンの開始行 + if y < r # 改行が必要なら + p "\n" * (r - y) # 改行を挿入して + x = 0 # 列の先頭へ戻る + end + c = l.start_column # トークンの開始列 + if x < c # 空白が必要なら + p " " * (c - x) # 空白を挿入 + end + p ruby(t) # トークン本体を出力 + y = l.end_line # 現在行を更新 + x = l.end_column # 現在列を更新 +} +``` + +補足: 変数名がやたら短いのは、このあとの振り仮名データの量を削減するため。 + +トークン種別に応じた色付けは CSS でおこなっている。出力する HTML のクラス名に `Prism::Token#type` を指定しておいて、`index.html` でそれぞれのクラスにスタイルを当てた。 + +```html + <style> + /* ... */ + + .COMMENT { + color: #777; + font-style: italic; + } + + .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER { + color: #088; + } + + /* ... */ + </style> +``` + +トークン種別の列挙にはそれなりに文字数を使ってしまうのだが、今回の TRICK のレギュレーションでは `index.html` にサイズ制限がなかったので好きに色を付けることができた。 + +{#ruby-text} +## 振り仮名 + +それぞれの英単語や記号に対応した振り仮名のデータは、プログラム中に埋め込まれている。 + +```ruby +def rt(t) + r = { + :"&&" => "1136", + :"=" => "04199275", + :"||" => "623147", + :$s => "41750825", + :* => "111775", + # ... + type: "310455", + utf_8: "70923803920853080440", + value: "48746992", + x: "08351525", + y: "7904", + } + kana( + r[:"#{t.type}"] || + r[s = :"#{t.value.downcase}"] || + s.end_with?(":") && r[:"#{s[..-2]}"] || + nil + ) +end +``` + +トークンの種類 (`t.type`) またはトークンの文字列表現そのもの (`t.value.downcase`) を使ってテーブルを引いて振り仮名へ変換している。 +このテーブルのキー部分そのものにも振り仮名を振るために、トークンが `:` で終わっていれば `:` を取り除いて振り仮名を得ている (例: `"value:"` → `"value"` → `"48746992"`)。 + +このテーブルはサイズ制限を突破するために圧縮されており、`kana()` 関数で展開される。 + +```ruby +def kana(s) + s + &.scan(/.{2}/) + &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)} + &.*("") +end +``` + +例えば `value` に対応する振り仮名データ `"48746992"` であれば、次のような変換を経て振り仮名へと展開される。 + +```ruby + s + # => "48746992" + &.scan(/.{2}/) + # => ["48", "74", "69", "92"] + &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)} + # => ["バ", "リ", "ュ", "ー"] + &.*("") + # => "バリュー" +``` + +これは後で気付いたのだが、Ruby は多倍長整数が扱えるので `"48746992"` のようなデータは単に `48746992` と書けばよかった。 +`kana()` 関数が多少長くはなるが、振り仮名データの数 x 2 バイト分サイズが減るのでこちらの方が短くなる。 +サイズ制限の都合で振り仮名を振るのを諦めた記号もあったのでもったいない。 + +{#outro} +# おわりに + +本っ当に取りたかったので心から嬉しいです。 +全部で 3作提出したのですが、他の 2つも選外佳作として選出していただけた上、そのうちの "Least Truthful" については最後に Matz 氏から言及があり、審査員賞と合わせて望外の栄誉となりました。 + +ありがとうございました! diff --git a/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/screenshot.png b/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/screenshot.png Binary files differnew file mode 100644 index 00000000..0bfe3be9 --- /dev/null +++ b/services/blog/content/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/screenshot.png diff --git a/services/blog/content/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos.dj b/services/blog/content/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos.dj new file mode 100644 index 00000000..b64b7981 --- /dev/null +++ b/services/blog/content/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos.dj @@ -0,0 +1,73 @@ +--- +[article] +uuid = "087e98f2-743c-48d8-9f67-e9b57e354845" +title = "【Composer】 composer-patches v2 では macOS でも GNU patch のインストールが不要になる (予定)" +description = "composer-patches は BSD patch に対応しておらず、一部のパッチの適用に失敗する。現在ベータ版である v2 では patch コマンドに依存しなくなり、macOS で使うときのストレスが解消される見込み。" +tags = [ + "composer", + "macos", + "php", +] + +[[article.revisions]] +date = "2025-04-10" +remark = "デジタルサーカス株式会社の社内記事として公開" +isInternal = true + +[[article.revisions]] +date = "2025-04-24" +remark = "公開" +--- +::: note +この記事は、2025-04-10 に [デジタルサーカス株式会社](https://www.dgcircus.com/) の社内 Qiita Team に公開された記事をベースに、加筆修正して一般公開したものです。 +::: + +{#intro} +# はじめに + +[Composer](https://getcomposer.org/) は PHP におけるデファクトスタンダードなパッケージ管理システムである。 + +Composer を拡張するプラグインの一つに、[composer-patches](https://github.com/cweagans/composer-patches) という Composer パッケージがある。 +これは、Composer でパッケージをインストールするときにそのパッケージへ任意のパッチを当てるプラグインである。 + +社内で発見しすぐに適用しなければならないバグ修正や、Pull Request こそあるもののなかなかマージされない機能等をすぐさま適用してリリースすることができる。 + +弊社でも多くのプロジェクトで活用されており、のべ数では数百ものパッチが当てられている。 + +{#on-macos} +# macOS での問題点 + +`composer-patches` は、macOS で一部のパッチの適用に失敗することが知られている。 +関連 issues: + +* https://github.com/cweagans/composer-patches/issues/522 +* https://github.com/cweagans/composer-patches/issues/326 + +これは、`composer-patches` の想定する `patch` コマンドが GNU 実装の patch であることに由来する。 +macOS にプリインストールされている `patch` はいわゆる BSD patch であり、GNU patch とは完全な互換性がない。 + +ワークアラウンドとして、macOS にも GNU patch をインストールしてしまうという方法がある。 +例: + +``` +$ brew install gpatch +$ echo 'PATH="/opt/homebrew/opt/gpatch/libexec/gnubin:$PATH"' >> ~/.zshrc +``` + +GNU patch を Homebrew などの手段でインストールし、BSD patch よりも優先されるパスに配置すれば問題が解消する。 + +{#in-version-2} +# v2 では + +現在ベータ版である `composer-patches` v2 では、このワークアラウンドが不要になる (見込み)。 + +最新の実装では、`git apply` コマンドが最優先で使われる。 +また、Git リポジトリがない場合 (`config.preferred-install` を `dist` に設定している場合など。デフォルトではそうなる) には `git init` を使って一時的にリポジトリを作成し、その上で `git apply` を実行するようになった。 + +この変更により、環境ごとに差異のある `patch` コマンドへの依存がなくなるので、macOS で `composer-patches` を使うときの厄介事は解消されるものと思われる。 + +[2.0.0-beta1](https://github.com/cweagans/composer-patches/releases/tag/2.0.0-beta1) のリリースノートより: + +> * Only have git patchers and freeform patcher? by [*@cweagans*](https://github.com/cweagans) in [#472](https://github.com/cweagans/composer-patches/pull/476) + +この変更で `patch` コマンドへの依存が排除された。 diff --git a/services/blog/content/posts/2025-05-05/make-tiny-self-hosted-c-compiler.dj b/services/blog/content/posts/2025-05-05/make-tiny-self-hosted-c-compiler.dj new file mode 100644 index 00000000..51046f22 --- /dev/null +++ b/services/blog/content/posts/2025-05-05/make-tiny-self-hosted-c-compiler.dj @@ -0,0 +1,290 @@ +--- +[article] +uuid = "64f5e1a6-2f5c-4d5d-b1c8-8346a66c1d40" +title = "セルフホスト可能な C コンパイラを作った" +description = "ゴールデンウィークを使って、セルフホストできる C コンパイラを開発した。" +tags = [ + "c", +] + +[[article.revisions]] +date = "2025-05-05" +remark = "公開" +--- +{#intro} +# はじめに + +C コンパイラと言えば、世界三大自作したいソフトウェアの一角である。 +というわけで [『低レイヤを知りたい人のためのCコンパイラ作成入門』](https://www.sigbus.info/compilerbook) (以下 compilerbook) 片手に作ることにした。 + +実装する機能を適切に絞ってやればゴールデンウィークの間 (2025-05-03 から 2025-05-06) にセルフホストまで持っていけるのではないか?という仮説を立て、ISO 8601 の表記で 4日間を表す "P4D" を冠して P4Dcc と名付けた。 + +[P4Dcc のリポジトリはこちら](https://github.com/nsfisis/P4Dcc) + +{#regulation} +# レギュレーション + +* 実装するのは C 言語からアセンブリ言語への変換部分のみ。アセンブラやリンカは GCC をそのまま用いる +* compilerbook を読みながら実装してよい +* compilerbook に記載されたソースコードを除き、コンパイラのソースコードを読まない +* GCC の出力は見てもよい。それ以外のコンパイラの出力 (特に 9cc などの compilerbook 準拠のコンパイラ) は見ない +* ソースコードの生成やデバッグに AI を使わない。ツールの使用方法を調べる目的 (GCC に渡すフラグなど) には使ってよい + +{#design} +# 設計 + +ゴールデンウィークの4日間で終わらせたいので、実装する言語機能は最低限に絞ることが必要になる。 +今回は次のような設計とした (compilerbook の設計を踏襲しているものは除く)。 + +* 宣言の文法を単純にパースできるものに絞る + + * `typedef` をサポートしない + + * 構造体には必ず `struct` キーワードを書く + + * 配列型をサポートしない + + * 常にヒープに確保してポインタ経由で扱う + + * 以上の制限により、型に関する情報が必ず変数名の前に来る + +* 無くてもなんとかなる構文糖を実装しない。ソースを書くときに頑張る + + * インクリメント・デクリメント演算子 (1足したり引いたりする) + * 複合代入演算子 (左辺と右辺で 2回書く) + + * なお、`+=` と `-=` はセルフホスト達成後に実装された + + * `while` (`for` で置き換える) + + * なお、`while` はセルフホスト達成後に実装された + + * `switch` (`if` で置き換える) + * ほか多数 + +* プリプロセッサのほとんどを実装しない + + * 数値または識別子へ置換する単純な `#define` のみサポートする + * 特に、`#include` をサポートしないのは重要な設計判断。すべて 1ファイルでおこなう + +* グローバル変数を用いない + + * `stdin`、`stdout`、`stderr` を含む + * これは compilerbook とは大きく設計が変わった部分 + * これにより、トップレベルに来るのは関数か構造体の定義/宣言のみとなった + +* 変数のシャドウイングを実装しない + + * 変数は常に関数スコープ + * グローバル変数もないので、スコープチェーンの実装が不要になる + +{#language-features} +## 言語機能 + +最終的にサポートされた機能は以下のとおり。 + +* 文 + + * `if` / `else` + * `for` + * `break` + * `continue` + * `return` + * `while` (実装はセルフホスト達成後) + +* 式 + + * 二項演算 + + * `+` / `-` / `*` / `/` / `%` + * `==` / `!=` + * `<` / `<=` / `>` / `>=` + * `&&` / `||` + + * 代入 + + * `=` + * `+=` / `-=` (実装はセルフホスト達成後) + + * 単項演算: `-` / `!` / `*` / `&` / `sizeof` + * 関数呼び出し: `f(a, b)` + * 配列アクセス: `a[b]` + * メンバ呼び出し: `a.b` / `a->b` + * 整数リテラル + * 文字列リテラル + +* 型 + + * `char` + * `int` + * `long` + * `void` + * `struct` + * それらのポインタ + +* 宣言・定義 + + * 関数 + * 構造体 + +* プリプロセッサ + + * 引数なし `#define` + +{#development} +# 開発 + +時系列順に開発の様子を辿っていく。 + +{#day1} +## 1日目 (2025-05-03) + +compilerbook では整数一つのパース・コード生成から始めるが、今回は以下のようなソースをパースしてコード生成するところからスタートすることにした。 + +```c +int main() { + return 42; +} +``` + +この時点で、`struct Token`、`struct Parser`、`struct AstNode`、`struct CodeGen` といった主要なデータ構造が定義され、この後もほぼ同じソース設計のまま進めている。 + +compilerbook のようなインクリメンタルな進め方を取らずに、最初から普通の言語処理系のような構成にしたのには理由がある。 + +それは、どのくらいの言語機能があればコンパイラを作るのに十分かをこの時点で見積もるためである。 +開発を開始する前にも必要な言語機能にはあたりを付けていたが、実際にプロトタイプを作ってみて、これだけの機能セットがあれば足りるだろうという正確な TODO リストを作りたかった。 +実際、このとき作ったチェックリストはこのあともほとんど変わっていない (大きな変化点は、配列型をサポートしないと決めたことくらいか)。 + +このあとは、おおむね compilerbook に従って以下のように機能追加を続けた。 + +1. 四則演算 +1. 単項マイナス +1. 比較 +1. ローカル変数 +1. `if` 文 +1. `for` 文 +1. 引数なしの関数呼び出し +1. 引数ありの関数呼び出し +1. 文字列リテラル + +一日の終わりには、次のようなプログラムのテストが通るようになった。 + +```c +int printf(); + +int main() { + int i; + for (i = 1; i <= 100; i = i + 1) { + if (i % 15 == 0) { + printf("FizzBuzz\n"); + } else if (i % 3 == 0) { + printf("Fizz\n"); + } else if (i % 5 == 0) { + printf("Buzz\n"); + } else { + printf("%d\n", i); + } + } + return 0; +} +``` + +{#day2} +## 2日目 (2025-05-03) + +この時点で、不足している機能はおおよそ2つ。ポインタと構造体である。 + +このあたりからは compilerbook の解説も減っていき (構造体については完全に記載がない)、実装も離れていっている。 + +以下のように実装を進めていった。 + +1. `char`、`long`、`void` +1. ポインタ +1. アドレス演算子: `&` +1. 間接参照演算子: `*` +1. `sizeof` +1. ポインタの演算 +1. `#define` +1. 構造体の定義・宣言 +1. 構造体の `sizeof` +1. メンバーアクセス +1. 論理演算子: `&&`、`||` +1. 初期化式つきの変数定義 +1. 文字リテラル +1. 配列アクセス +1. 論理演算子: `!` +1. 返り値なしの `return` + +`&`、`*`、`sizeof` あたりの実装が終わるとかなり C 言語らしくなっていき楽しい。 + +このあたりから、セルフホストに向けて逆方向からのアプローチも並行しておこなっている。 +セルフホストするためには処理系のソースコードで使っている言語機能をすべて実装する必要があるわけだが、これまでは処理系が扱える機能を拡充していくという方向だった。この逆、つまり処理系のソースコードで使っている機能を減らすことでもセルフホストに近付いていく。 + +例えば、このコンパイラは `typedef` をサポートしていないが、開発中ずっと `typedef` を使わないというのは面倒だ。 +そこで、セルフホストがある程度現実的になるまでは構造体を `typedef` しておいて、途中のどこかで `typedef` を手で脱糖する。 + +これらの作業をおこなうことで、処理系自身のソースコード `main.c` をパースしてバイナリを出力することができるようになった。 +いわゆる第2世代のコンパイラである。この現時点ではまだ第2世代コンパイラは何もできない (何を与えてもクラッシュする)。 + +{#day3} +## 3日目 (2025-05-03) + +さて、第2世代コンパイラが手に入ったので、ここからは地獄のデバッグ作業が始まる。多段になっているために問題が起きている箇所の特定が難しい。 + +......と考えていたのだが、実際のところデバッグは1時間ほどで終わってしまった。 +修正したのは1点のみ。 +なんのことはない、2日目終了時点でほとんど完成していたわけだ。 + +記念すべき (?) 最後のバグはこちら。 + +```diff + gen_expr(g, ast->expr1, GEN_RVAL); + } else { + gen_expr(g, ast->expr1, GEN_RVAL); +- gen_lval2rval(ast->expr1->ty); ++ gen_lval2rval(ast->expr1->ty->to); + } + } +``` + +メモリアドレスから参照先の値を得る際、その型によってロードする命令の種類を変える必要があるのだが、その切替をポインタ型でおこなっていた。 +正しくは、そのポインタ型が指す型を元にして切り替えなければならない。 + +これを修正すると、第2世代コンパイラが第3世代コンパイラを出力できるようになり、その後も第N世代が第N+1世代を生成できるようになった。 + +あとは、第2世代のコンパイラがそれ以降のコンパイラとバイナリレベルで一致するかどうかを確かめればよい。 +実際に調べてみると、ほとんどの場所が一致したもののどの世代も 6バイトだけ異なることがわかった。 + +一体どこが異なるのか。`hexdump` の差分がこちら。 + +``` +$ diff -u <(hexdump -C p4dcc2) <(hexdump -C p4dcc3) +@@ -5090,7 +5090,7 @@ + 00015db0 72 72 61 79 5f 65 6e 74 72 79 00 66 72 61 6d 65 |rray_entry.frame| + 00015dc0 5f 64 75 6d 6d 79 00 5f 5f 66 72 61 6d 65 5f 64 |_dummy.__frame_d| + 00015dd0 75 6d 6d 79 5f 69 6e 69 74 5f 61 72 72 61 79 5f |ummy_init_array_| +-00015de0 65 6e 74 72 79 00 63 63 6d 69 42 49 59 6b 2e 6f |entry.ccmiBIYk.o| ++00015de0 65 6e 74 72 79 00 63 63 53 71 64 47 76 57 2e 6f |entry.ccSqdGvW.o| + 00015df0 00 66 61 74 61 6c 5f 65 72 72 6f 72 00 72 65 61 |.fatal_error.rea| + 00015e00 64 5f 61 6c 6c 00 74 6f 6b 65 6e 69 7a 65 00 74 |d_all.tokenize.t| + 00015e10 79 70 65 5f 6e 65 77 00 74 79 70 65 5f 6e 65 77 |ype_new.type_new| +``` + +`fatal_error`、`read_all`、`tokenize` `type_new` はいずれも `main.c` で定義された関数の名前である。 +このことから考えると、これは GCC が埋め込んだシンボルテーブルである可能性が高い。 +わずかに異なっている 6バイトは、ランダム生成された何かのように見える。 + +そこで `gcc` に `-s` (シンボルテーブルを削除するフラグ) を渡してみると、めでたく2世代目以降のコンパイラのバイナリが完全に一致するようになった。 + +これにてセルフホスト達成である。 + +{#outro} +# おわりに + +最終的な実装は1900行ほど、所要時間は20時間弱となった。 + +正直なところ、思ったより早く終わって拍子抜けしている。 +これは compilerbook がうまく実装順を整理しているのと、アセンブリの細かい落とし穴を事前に解説して潰していることが大きいと思われる。 + +当初の仮説どおり、サポートする機能を慎重に選ぶことにより短期間でセルフホストまで持っていくことができた。 +案外簡単に作れてしまうので、まとまった休みに是非いかがだろうか。 diff --git a/services/blog/content/posts/2025-06-14/baba-is-you.dj b/services/blog/content/posts/2025-06-14/baba-is-you.dj new file mode 100644 index 00000000..01864679 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you.dj @@ -0,0 +1,446 @@ +--- +[article] +uuid = "127019eb-e83f-4c9e-ab54-2021124f1bbb" +title = "最高のパズルゲーム Baba Is You をやれ" +description = "Baba Is You という最高のパズルゲームをクリアした。是非プレイしてほしい。" +tags = [ + "game", +] + +[[article.revisions]] +date = "2025-06-14" +remark = "公開" + +[[article.revisions]] +date = "2025-06-15" +remark = "後半 (ネタバレあり) のとある面について追記" +--- +{#intro} +# Baba Is You とは + +[Baba Is You](https://www.hempuli.com/baba/) という倉庫番系パズルゲームがある。 +私がこれまでプレイしたことのあるパズルゲームの中で、間違いなく最高のパズルゲームだと断言できる。 +これより面白いパズルゲームを知っている人は、絶対に買うので教えてほしい。 + +すでに押しも押されもせぬ傑作としての名をほしいままにする本作だが、名作の感想はいくつあってもいいので書く。 + +前半はネタバレなし、後半はネタバレありで書くので、プレイしていない人は前半まで読んだら閉じてほしい。 + +{#no-spoiler} +# 前半 (ネタバレなし) + +{#what-is-baba-is-you} +## どういうゲームか? + +Baba Is You はいわゆる倉庫番パズルの一種である。 +2D のグリッドで操作キャラを動かし、アイテムを押して動かすことでパズルを解く。 + +Baba Is You の特異な点は、倉庫番のルールが盤面上で動かせるオブジェクトとして配置してある点にある。 +これは Baba Is You の一番最初の面である。 + + + +ここには次のようなルールがある。 + +* `BABA` `IS` `YOU` + + * Baba はあなた (操作キャラ) + +* `ROCK` `IS` `PUSH` + + * 岩は押せる + +* `WALL` `IS` `STOP` + + * 壁は止まる (押せない) + +* `FLAG` `IS` `WIN` + + * 旗は勝ち + +最初の状態では、`YOU` である baba (うさぎや猫のような白い生き物) が `WIN` である旗に触れることで勝利条件を満たしクリアとなる。 + +これらのルールを構成しているテキストを押して動かすことで、ルールをさまざまに変化させることができる。 +この面なら一例として次のようなルールが作れるだろう。 + +* `FLAG` `IS` `YOU` + + * 旗が操作キャラになり、キー入力で動かせる + +* `ROCK` `IS` `STOP` + + * 岩が押せなくなる + +* `BABA` `IS` `WALL` + + * Baba が壁へと変化し、操作不能になる + + * `WALL` `IS` `YOU` を同時に作っていればその限りでない! + +この「ルール自体を変えられる」という性質により、パズルの難易度・複雑さが大きく上がっている。 +プレイヤーは、どのオブジェクトを `YOU` にするのか、`WIN` にすべきは何か、どれに `PUSH` を付けるべきか、いつどの順番でルールを変えるのか、今の手札で作れるルールは何か等と悩みながら、次第に難しくなるパズルと格闘しなければならない。 + +{#play-time-and-difficulty} +## ゲームのボリューム・難易度 + +遊べる面の数は「200 以上」ある (Steam ストアページの表記より引用。正確な個数はここでは控える)。 +ただしこれは追加 DLC の分を含んでいないので、実際には更に大量にある。 + +パズルゲームなのでプレイ時間には大きくブレがあるだろうが、私の場合はノーヒントで 75 時間弱だった。 +ゲーム画面を閉じて紙とペンで考えていた時間を含めれば、+10~+20時間といったところだろうか。 + +パズルの難易度はべらぼうに高い。 +ひとつ解くのに数時間かかったり、数日間ひとつも解けなかったりするのはよくある (あくまで全クリを目指す場合)。 + +完全クリア以外にもいくつかマイルストーンはあるので、それを目指すのもありだろう。 + +{#appeal} +## 魅力 + +何がこのゲームを傑作たらしめているのか。 + +{#very-difficult} +### 高い難易度 + +すでに書いたが、このゲームは非常に難しい。 +ゲームのルールを変えられると聞くと、何でもありの大味なプレイ体験かのように思えるかもしれない。 +しかし、適当にルールを弄り回して解けるような面は最序盤くらいにしかなく、ゲームが進んでいくと総当たりすら困難になっていく。 +解けない、やれることは全部試したはずだ、ゴールから逆算してもこれ以外ありえないのに実現できない、とにかく解けない。 +何度もそう思うことになるだろう。 + +それにもかかわらず、理不尽だと感じることは驚くほど少ない。 +隠された法則・秘密のルールがないというわけではない。 +最初の面を再び例に出そう。 +`ROCK` `IS` `PUSH` と `ROCK` `IS` `STOP` を同時に成立させたら、岩は押せるのか押せないのか。 +`PUSH` と `STOP` の優先順は言葉で説明されるわけではないし、自分で試して規則を発見することが求められる。 +しかし、実際にゲーム上で試しさえすれば、その規則は明らかな結果となってプレイヤーへと提示されるのである。 +これを繰り返すことで、プレイヤーは単語ごとの挙動を、そして Baba Is You を理解していく。 + +この特徴により、その難易度に比して理不尽さが大きく低減されていると感じる。 + +{#very-flexible} +### 新しい単語との出会い + +このゲームには `PUSH`、`STOP`、`WIN`、`YOU` 以外にもさまざまな単語がある。 +新しい単語が導入されるときは大抵チュートリアル用の簡単な面が用意されており、プレイヤーはそこで新単語を使っていろいろと実験をすることになる。 + +それらの単語の中には、一目で「危険」だとわかる奴らがいる。 +`YOU` や `WIN` は最初からいる連中だが、普通のパズルゲームなら操作キャラや勝利条件を変えられるだけでもとんでもないルールブレイカーだろう。 + +危険な匂いのする単語と出会ったときの「こんな単語を許したらとんでもないことになるぞ」という感覚は実際にプレイしなければ味わえない。 + +{#very-beautiful} +### 美しいパズル + +私が大好きなとある面の話をしよう。 +この面は最終盤に出現する。 +これは序中盤で出てきたとある面のリメイクであり、ほんの少しだけ手が加えられている。 +オリジナルとリメイク版の差分は1マスの窪みがあるかないか。 +この一つの差分だけで、難易度が劇的に上昇している。 +片や特筆することのない印象の薄い面、片やゲーム内屈指の高難易度面である。 +最終盤にあるがゆえになんとか解けるものの、もし配置順が入れ替わりでもしようものなら (難易度の差を考えれば絶対にありえないことだが)、ほとんどのプレイヤーがここで諦めるだろう。 + +これを解いたときは、たった1マスの差でこれだけ難しくできるものなのかと感動した。 +他にも似たような例はいくつもある。 +素晴しい出来の美しいパズルに何度も何度も出会うことができる。 + +{#play-now} +## Baba Is You をやれ + +Baba Is You は最高のパズルゲームである。 + +お世辞にも簡単だとは言えないが、苦しむ価値のあるゲームである。 + +この次のセクションからはネタバレありの感想を書くが、プレイしていない人はもちろん、プレイ中で完全クリアしていない人も読まないことを勧める。 +その価値があるゲームだと保証する。 + +{#spoiler} +# 後半 (ネタバレあり) + +ではここからは完全クリアしたプレイヤーに向けて話そう。 + +ここでいう「完全クリア」はレベルパックの「Baba Is You」(いわゆる本編) に用意されているパズルをすべて解いた状態を指すことにする。 +Steam の場合、全実績解除と読み替えてもよい。 +すなわち、「Museum」や「New Adventures」を含まない。 + +{#notation} +## 表記 + +ゲーム上のオブジェクトについて次のように表記することにする。 + +* `BABA`: テキストとしての `BABA` +* Baba: オブジェクトとしての baba +* `A`、`B` など: 任意のテキスト + + * そういうテキストが出てくる面もあるがその面の話はしない + +* A、B など: 任意のオブジェクト +* `A/B`: `A` と `B` のテキストが重なった状態 + +また、個々のパズルのことはここまでと同様に「面」と呼ぶことにする。 +「`LEVEL` というテキストが指すゲーム上のオブジェクト」は「level」と書く。 + +{#impressive-levels} +## 印象的な面 + +ここからは印象的な面を語っていく。 + +{#map} +### MAP + +{#submerged-ruins-and-sunken-temple} +#### SUBMERGED RUINS、SUNKEN TEMPLE + + + + + +ここまでスルスル解けていて初めてしばらく止まった面。 +また、苦戦して解いた次の面がその面の派生で絶望するという経験をした最初の面。 +この瞬間が苦しくもあり楽しくもある。 + +{#prison-and-dungeon} +#### PRISON、DUNGEON + + + + + +高難易度面で当然のように要求されるテクニックの初出。 +可能な行動が大きく制限されているのでマシだが、それでも初見時には困惑した。 + +{#further-fields} +#### FURTHER FIELDS + + + +お気に入りの面。 +`MOVE` を活用するのも `YOU` を一時的に消すのも好きなので、両方出てくるこの面は大好き。 + +{#scenic-pond} +#### SCENIC POND + + + +はい。まあこいつは後で触れることにしよう。 + +{#concrete-goals} +#### CONCRETE GOALS + + + +これも初見時に苦戦した面。 +PRISON などと同様に一度理解すれば何ということのない面だが、最初に解けたときは偶然だった。 +`FLAG` `IS` `WIN` がギリギリ取り出せそうに「見える」のが嫌らしい。 + +{#lock-the-door} +#### LOCK THE DOOR + + + +`SHIFT` 重ねの初出面。 +このテクニックを再び使うのは終盤になってからであり、私はそのときにはもう `SHIFT` 重ねを忘れていたので大苦戦した。 +戯れにスロット2を使って2周目をやっていてこの面まで到達し、そこでようやく `SHIFT` 重ねが `MOVE` もどきになることを思い出した。 +その意味でも印象深い面。 + +{#insulation} +#### INSULATION + + + +MAP の前半 (~DEEP FOREST) では最も苦戦した面。 +`SWAP` の理解が固まっておらず、「こういう状況が作れたら解けそうだ」という勘が働かなかった。 +正直なところ `SWAP` は今も苦手意識がある (終盤で強制的に学ばされる `SHIFT` と違って、それほど高難度面での出番がないのも大きいと思う)。 + +{#bottleneck} +#### BOTTLENECK + + + +MAP の中で一番苦しんだ面。 +実は一度ここで投げて諦めたのだが、`EMPTY` を理解した今となっては脳内でも瞬殺できるくらい簡単になってしまった。 +最初に面を見てから解き終わるまでの時間は間違いなく最長で、半年以上かかっている (他は長くとも数日間)。 + +{#heavy-cloud} +#### HEAVY CLOUD + + + +難しい面ではあるのだが、それ以上に解法の美しさに感動した面。 +解き終わった後に思わず「美しい......」と呟いてしまったのはこの面だけだった。 +Baba Is You の好きな面はと聞かれれば真っ先にこれを挙げる。 + +{#adventurers} +#### ADVENTURERS + + + +難所の多い FLOWER GARDEN の癒し。Hand が `MOVE` と `SHIFT` でガチャガチャ動くのを見るのが楽しい。 + +{#out-at-sea} +#### OUT AT SEA + + + +MAP の問題児。正攻法がテキスト重ねである最初の面。 +この面、`ICE/LAVA` `IS` `PUSH` を作ったあと重なった ice と lava を (`ICE` `IS` `PUSH` だけ作るなどして) 分離しないといけないのだが、意気揚々と ice on lava の状態で door に向かって push して push できなかったときの感情はよく覚えている。 +テキスト同士を重ねて `A` `IS` `PUSH` と `B` `IS` `PUSH` を両立させるというぶっ飛んだアイデアを実現してもなお解けないのか、この方針がまさか間違っているなどということがあるのか、いやそんなはずはない......。 +実際のところそこからのリカバリーはすぐできたが、そのときの絶望はこれまででも最大であった。 + +{#seeking-acceptance} +#### SEEKING ACCEPTANCE + + + +ここも好きな面。 +FURTHER FIELDS の精神的後継のようなものなので当然かもしれない。 +せっせと働く bird がかわいい。 + +{#fragile-existence} +#### FRAGILE EXISTENCE + + + +MAP の印象的な面と言えば、これを取り上げないわけにはいかない。 +`LEVEL` `IS` `A` による level の変換がおこなえる初の面である。 +ここまで Baba Is You を進めたプレイヤーであれば、初出のテキストが現れたらまずはその場の色々なテキストと組み合わせてみて相互作用を確認する。 +それを見事に利用されたというか、気付かずにはいられないように仕向けられているというか、本当によくできたゲームである。 + +{#map} +#### MAP + +MAP 自身。FRAGILE EXISTENCE でそれに気付いたなら当然 HOSTILE ENVIRONMENT でも気付くし、MAP で baba が操作できることに気付いたならもちろん右下のルールに目を向ける。 +次に考えるのは無論こうだ。ここで flag を取ったらどうなるんだ? +このゲームはその疑問に答えてくれる。期待をはるかに上回る形で。 + +??? でまたしても待ち構える `BABA` `IS` `YOU` と不穏な `LEVEL` のテキスト、そして GLITCH の `W` `E` `L` `C` `O` `M` `E`。 +間違いなく最高のパズルゲームだと確信した。 + +{#triple-question} +### ??? + +{#vip-area} +#### VIP AREA + + + +??? で大いに苦戦した面のひとつ。 +PRISON と DUNGEON で既出のテクニックが肝だが、ちと離れすぎじゃないのか。 +この面のリメイクもあるが、ここで苦しんだからかそちらはあまり苦戦しなかった。 + +{#ultimate-maze} +#### ULTIMATE MAZE + + + +普通に解くだけなら大したことのない面だが、問題はこれの `LEVEL` `IS` `TEXT` 解である。 +出現順に書いているのでここに置いたが、解いたのはもっと後、META の後半に差しかかった頃になる。 +??? コンプリートの実績が取れていないことに気付き、残っているとすればここの `LEVEL` `IS` `TEXT` しかないと考えたまではよかったが、そこからが大変だった。 +個人的にこのゲームで一番苦しかったのがここの `TEXT` 変換解である。 +単純な難しさに加え、実績が取れていない原因がこの面だという確信も持てなかったので、解けるかどうかわからない状態で挑み続けることとなり疲弊した。 + +{editat="2025-06-15" operation="追記"} +::: edit +??? の DO IT YOURSELF 以降を開くにはこの面の `LEVEL` `IS` `TEXT` が必須だと思っていたのだが、どうもそうではないらしい。 +いずれにせよそれを思いつけなかったので同じことか。 +::: + +{#stardrop-and-meteor-strike} +#### STARDROP、METEOR STRIKE + + + + + +両方とも難しい面ではあったが、単文字のテキストが綺麗に活用された美しい面として印象に残っている。 +`G` `R` `A` `S` `S` `IS` `H` `O` `T` をこれほど無駄なく使えるとは! + +{#getting-together} +#### GETTING TOGETHER + + + +初見のインパクト大にして難易度も相応に高い良作。 +この頃はまだ `SHIFT` を_理解_していなかったので大変だったが、ここを越えたことでむしろこの後の難所が楽になったと言える。 + +{#depths} +### DEPTHS + +??? といういかにもクリア後のオマケっぽい名前のマップを攻略したらまだまだ深淵が待ち構えていた。 + +{#crushers} +#### CRUSHERS + + + +DEPTHS の序盤で道を塞いでいる必須面であるにもかかわらず圧倒的難易度で立ちはだかる凶悪な面。 +大苦戦した挙句 `LEVEL` `IS` `BELT` を作ってアレ?となったのは私だけではないはず。 + +{#parade} +#### PARADE + + + +取れる行動が多いこと、もう少しで解けそうなルートが多いこと、そのどれもが一筋縄ではいかないこと。 +それらがすべて揃った高難度面。 +昔のバージョンでは ??? に置いてあったらしい。そんなバカな。 + +{#meta} +### META + +ここではもう覚悟していたので続きがあることには驚かなかったが、明らかに不穏な `CURSOR` に震えつつ先へ進むことになる。 + +{#booby-trap} +#### BOOBY TRAP + + + +難しいとか難しくないとかじゃなくここは触れざるをえない。 +`FLAG/TEXT` 解以外はそれほど苦戦しなかったが、とにもかくにもクリアの要求回数が多すぎる。 +もちろん最短で進められるなら別だが、META での試行錯誤のためにはこいつの形を毎回変えなければならない。 +しかもどの変換もそれなりにステップ数を要するのが厄介である。 +印象に残った面であるのは確か。 + +{#the-box} +#### THE BOX + + + +よくぞこの面を作ってくれた。 +外の level を参照させるギミックは、`LEVEL` `IS` `A` の変換をやりだした頃からいつかあるはずと思っていたので、そのとおりのパズルが出てきてくれて嬉しい。 +これぞ Baba Is You。 + +{#the-return-of-scenic-pond} +#### THE RETURN OF SCENIC POND + + + +前半のネタバレなし感想にも書いたが、これは SCENIC POND のリメイクであり、ほとんど差異がない。 +たった1マス窪みが無くなっただけである。 +それだけでここまで難しくできるのか。 + +最終盤に配置されていたことで難易度の割には苦戦しなかったが、難易度以上にリメイクの美しさに感動した面。 + +{#difficult-levels} +## 初見時難易度ランキング + +最後に、初見時の難易度を 10 位までランキングにしてみた。 +あくまで初見のときの難易度なので、面自体の難易度ではない。 +解くのにかかった時間とも少し違う。 +あえて言うなら苦しんだ順。 + +1. ULTIMATE MAZE (`TEXT` 解) +1. CRUSHERS (`TEXT` 解) +1. PARADE +1. BOTTLENECK +1. BOOBY TRAP (`FLAG/TEXT` 解) +1. THE RETURN OF SCENIC POND +1. OUT AT SEA +1. GETTING TOGETHER +1. VIP AREA +1. STARDROP + +{#outro} +# おわりに + +神ゲー。プレイ済みの人は会ったとき一番好きな面の話でもしましょう。 diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ADVENTURERS.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ADVENTURERS.jpeg Binary files differnew file mode 100644 index 00000000..8ba97a06 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ADVENTURERS.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BABA_IS_YOU.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BABA_IS_YOU.jpeg Binary files differnew file mode 100644 index 00000000..69c8bb5e --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BABA_IS_YOU.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOOBY_TRAP.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOOBY_TRAP.jpeg Binary files differnew file mode 100644 index 00000000..f993ac34 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOOBY_TRAP.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOTTLENECK.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOTTLENECK.jpeg Binary files differnew file mode 100644 index 00000000..9080626e --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_BOTTLENECK.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CONCRETE_GOALS.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CONCRETE_GOALS.jpeg Binary files differnew file mode 100644 index 00000000..a11be095 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CONCRETE_GOALS.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CRUSHERS.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CRUSHERS.jpeg Binary files differnew file mode 100644 index 00000000..5a29e603 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_CRUSHERS.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_DUNGEON.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_DUNGEON.jpeg Binary files differnew file mode 100644 index 00000000..9ed96771 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_DUNGEON.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FRAGILE_EXISTENCE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FRAGILE_EXISTENCE.jpeg Binary files differnew file mode 100644 index 00000000..693958dd --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FRAGILE_EXISTENCE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FURTHER_FIELDS.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FURTHER_FIELDS.jpeg Binary files differnew file mode 100644 index 00000000..866485b9 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_FURTHER_FIELDS.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_GETTING_TOGETHER.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_GETTING_TOGETHER.jpeg Binary files differnew file mode 100644 index 00000000..6d9879d2 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_GETTING_TOGETHER.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_HEAVY_CLOUD.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_HEAVY_CLOUD.jpeg Binary files differnew file mode 100644 index 00000000..78479628 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_HEAVY_CLOUD.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_INSULATION.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_INSULATION.jpeg Binary files differnew file mode 100644 index 00000000..16c8d091 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_INSULATION.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_LOCK_THE_DOOR.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_LOCK_THE_DOOR.jpeg Binary files differnew file mode 100644 index 00000000..4f7acf79 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_LOCK_THE_DOOR.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_METEOR_STRIKE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_METEOR_STRIKE.jpeg Binary files differnew file mode 100644 index 00000000..26209ea8 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_METEOR_STRIKE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_OUT_AT_SEA.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_OUT_AT_SEA.jpeg Binary files differnew file mode 100644 index 00000000..0a6fb4df --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_OUT_AT_SEA.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PARADE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PARADE.jpeg Binary files differnew file mode 100644 index 00000000..5bd1fe77 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PARADE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PRISON.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PRISON.jpeg Binary files differnew file mode 100644 index 00000000..21701abb --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_PRISON.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SCENIC_POND.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SCENIC_POND.jpeg Binary files differnew file mode 100644 index 00000000..76b80b7c --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SCENIC_POND.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SEEKING_ACCEPTANCE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SEEKING_ACCEPTANCE.jpeg Binary files differnew file mode 100644 index 00000000..e4c33fe8 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SEEKING_ACCEPTANCE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_STARDROP.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_STARDROP.jpeg Binary files differnew file mode 100644 index 00000000..f154f3b8 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_STARDROP.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUBMERGED_RUINS.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUBMERGED_RUINS.jpeg Binary files differnew file mode 100644 index 00000000..3581bc45 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUBMERGED_RUINS.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUNKEN_TEMPLE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUNKEN_TEMPLE.jpeg Binary files differnew file mode 100644 index 00000000..7f7659bb --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_SUNKEN_TEMPLE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_BOX.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_BOX.jpeg Binary files differnew file mode 100644 index 00000000..4c04ae9c --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_BOX.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_RETURN_OF_SCENIC_POND.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_RETURN_OF_SCENIC_POND.jpeg Binary files differnew file mode 100644 index 00000000..86edb294 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_THE_RETURN_OF_SCENIC_POND.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ULTIMATE_MAZE.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ULTIMATE_MAZE.jpeg Binary files differnew file mode 100644 index 00000000..97d3fe7e --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_ULTIMATE_MAZE.jpeg diff --git a/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_VIP_AREA.jpeg b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_VIP_AREA.jpeg Binary files differnew file mode 100644 index 00000000..6e352375 --- /dev/null +++ b/services/blog/content/posts/2025-06-14/baba-is-you/LEVEL_VIP_AREA.jpeg |
