From 4681e861efcdf9b0b73e3803e70712bc55162863 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 8 Aug 2026 10:38:18 +0900 Subject: feat(slides): improve slide controller --- services/nuldoc/public/blog/posts/2/index.html | 2 +- .../blog/posts/2021-03-05/my-first-post/index.html | 27 +- .../posts/2021-03-30/phperkaigi-2021/index.html | 2 +- .../index.html | 60 +- .../python-unbound-local-error/index.html | 51 +- .../ruby-detect-running-implementation/index.html | 45 +- .../ruby-then-keyword-and-case-in/index.html | 204 +++--- .../rust-where-are-primitive-types-from/index.html | 199 +++--- .../index.html | 41 +- .../vim-swap-order-of-selected-lines/index.html | 42 +- .../2022-04-09/phperkaigi-2022-tokens/index.html | 419 ++++++------ .../index.html | 5 +- .../posts/2022-05-01/phperkaigi-2022/index.html | 2 +- .../php-conference-okinawa-code-golf/index.html | 24 +- .../index.html | 2 +- .../index.html | 753 +++++++++++---------- .../phperkaigi-2023-unused-token-quiz-1/index.html | 105 +-- .../setup-server-for-this-site/index.html | 130 ++-- .../phperkaigi-2023-unused-token-quiz-2/index.html | 95 +-- .../phperkaigi-2023-unused-token-quiz-3/index.html | 333 ++++----- .../rewrite-this-blog-generator/index.html | 2 +- .../index.html | 696 +++++++++---------- .../2023-04-04/phperkaigi-2023-report/index.html | 2 +- .../2023-06-25/phpconfuk-2023-report/index.html | 2 +- .../compile-php-runtime-to-wasm/index.html | 252 +++---- .../index.html | 2 +- .../blog/posts/2023-12-03/isucon-13/index.html | 2 +- .../posts/2023-12-31/2023-reflections/index.html | 2 +- .../index.html | 260 +++---- .../index.html | 100 +-- .../2024-02-10/yapcjapan-2024-report/index.html | 2 +- .../2024-02-22/phpkansai-2024-report/index.html | 2 +- .../2024-03-17/phperkaigi-2024-report/index.html | 2 +- .../posts/2024-03-20/my-bucket-list/index.html | 2 +- .../phpcon-odawara-2024-report/index.html | 2 +- .../pipefail-option-in-gitlab-ci-cd/index.html | 119 ++-- .../index.html | 23 +- .../2024-05-11/phpconkagawa-2024-report/index.html | 2 +- .../2024-06-19/scalamatsuri-2024-report/index.html | 2 +- .../reparojson-fix-only-json-formatter/index.html | 107 +-- .../index.html | 87 +-- .../posts/2024-09-28/mncore-challenge-1/index.html | 2 +- .../posts/2024-12-04/cohackpp-report/index.html | 249 +++---- .../posts/2024-12-33/2024-reflections/index.html | 2 +- .../phperkaigi-2023-tokens-q1/index.html | 256 +++---- .../index.html | 19 +- .../phpcon-nagoya-2025-report/index.html | 2 +- .../index.html | 58 +- .../http-1-1-send-multiple-same-headers/index.html | 2 +- .../trick-2025-most-ruby-on-ruby-award/index.html | 153 +++-- .../index.html | 7 +- .../make-tiny-self-hosted-c-compiler/index.html | 80 +-- .../blog/posts/2025-06-14/baba-is-you/index.html | 2 +- .../partial-surrender-to-ebooks/index.html | 2 +- .../index.html | 61 +- .../index.html | 2 +- .../posts/2025-11-27/anybatross-writeup/index.html | 94 +-- .../archive-dynamic-site-with-wget/index.html | 94 +-- .../posts/2025-12-31/2025-reflections/index.html | 2 +- .../development-environment-2026/index.html | 2 +- .../rewrite-this-site-generator-2026/index.html | 2 +- .../i-can-eat-glass-it-doesnt-hurt-me/index.html | 22 +- .../regional-rubykaigi-sekigahara01/index.html | 2 +- services/nuldoc/public/blog/posts/3/index.html | 2 +- services/nuldoc/public/blog/posts/4/index.html | 2 +- services/nuldoc/public/blog/posts/5/index.html | 2 +- services/nuldoc/public/blog/posts/6/index.html | 2 +- services/nuldoc/public/blog/posts/7/index.html | 2 +- services/nuldoc/public/blog/posts/index.html | 2 +- 69 files changed, 2778 insertions(+), 2560 deletions(-) (limited to 'services/nuldoc/public/blog/posts') diff --git a/services/nuldoc/public/blog/posts/2/index.html b/services/nuldoc/public/blog/posts/2/index.html index 3621dd15..4ed53bb5 100644 --- a/services/nuldoc/public/blog/posts/2/index.html +++ b/services/nuldoc/public/blog/posts/2/index.html @@ -15,7 +15,7 @@ 投稿一覧 (2ページ目)|REPL: Rest-Eat-Program Loop - +
diff --git a/services/nuldoc/public/blog/posts/2021-03-05/my-first-post/index.html b/services/nuldoc/public/blog/posts/2021-03-05/my-first-post/index.html index 9896ebea..c36c2126 100644 --- a/services/nuldoc/public/blog/posts/2021-03-05/my-first-post/index.html +++ b/services/nuldoc/public/blog/posts/2021-03-05/my-first-post/index.html @@ -14,7 +14,7 @@ My First Post|REPL: Rest-Eat-Program Loop - +
@@ -146,25 +146,25 @@ list item 3 -
    -
  • +
      +
    • list item 1
    • -
    • +
    • list item 2 -
        -
      • +
          +
        • list item 2.a
        • -
        • +
        • list item 2.b
      • -
      • +
      • list item 3
      • -
      • +
      • list item 3
      @@ -175,7 +175,8 @@
      -
      puts "Hello, World!"
      +
      puts "Hello, World!"
      +

      emph strong @@ -226,7 +227,7 @@ name - + age @@ -236,7 +237,7 @@ Taro - + 10 @@ -244,7 +245,7 @@ Hanako - + 20 diff --git a/services/nuldoc/public/blog/posts/2021-03-30/phperkaigi-2021/index.html b/services/nuldoc/public/blog/posts/2021-03-30/phperkaigi-2021/index.html index 4b9cb683..28a146c8 100644 --- a/services/nuldoc/public/blog/posts/2021-03-30/phperkaigi-2021/index.html +++ b/services/nuldoc/public/blog/posts/2021-03-30/phperkaigi-2021/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2021|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html index db3f0334..ea909c94 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/cpp-you-can-use-keywords-in-attributes/index.html @@ -15,7 +15,7 @@ 【C++】 属性構文の属性名にはキーワードが使える|REPL: Rest-Eat-Program Loop - +
      @@ -72,41 +72,44 @@ タイトル落ち。まずはこのコードを見て欲しい。

      -
      #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; -
      }
      +
      #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
      +
      $ 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
      +

      コンパイルコマンド (C++17 指定)

      -
      $ clang --std=c++17 hoge.cpp
      +
      $ clang --std=c++17 hoge.cpp
      +

      この記事から得られるものはこれ以上ないので以下は蛇足になる。 @@ -136,8 +139,9 @@ 上のコードでは [[using]] をコメントアウトしているが、これは using キーワードのみ属性構文の中で意味を持つからであり、このコメントアウトを外すとコンパイルに失敗する。

      -
      // using の例 -
      [[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文
      +
      // using の例
      +[[using foo: attr1, attr2]] int x; // [[foo::attr1, foo::attr2]] の糖衣構文
      +

      C++17 の仕様も見てみる (正確には標準化前のドラフト)。 diff --git a/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html b/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html index a38d197c..aa63127f 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/python-unbound-local-error/index.html @@ -15,7 +15,7 @@ 【Python】 クロージャとUnboundLocalError: local variable 'x' referenced before assignment|REPL: Rest-Eat-Program Loop - +

      @@ -75,13 +75,14 @@ Python でクロージャを作ろうと、次のようなコードを書いた。

      -
      def f(): -
      x = 0 -
      def g(): -
      x += 1 -
      g() -
      -
      f()
      +
      def f():
      +    x = 0
      +    def g():
      +        x += 1
      +    g()
      +
      +f()
      +

      関数 g から 関数 f のスコープ内で定義された変数 x を参照し、それに 1 を足そうとしている。 これを実行すると x += 1 の箇所でエラーが発生する。 @@ -95,27 +96,29 @@ local変数 x が代入前に参照された、とある。これは、fx を参照するのではなく、新しく別の変数を g 内に作ってしまっているため。前述のコードを宣言と代入を便宜上分けて書き直すと次のようになる。var を変数宣言のための構文として擬似的に利用している。

      -
      # 注: 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()
      +
      # 注: 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()
      +

      当初の意図を表現するには、次のように書けばよい。

      -
      def f(): -
      x = 0 -
      def g(): -
      nonlocal x ## (*) -
      x += 1 -
      g()
      +
      def f():
      +    x = 0
      +    def g():
      +        nonlocal x   ## (*)
      +        x += 1
      +    g()
      +

      (*) のように、nonlocal を追加する。これにより一つ外側のスコープ (g の一つ外側 = f) で定義されている x を探しに行くようになる。 diff --git a/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html b/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html index 0dfc31fd..c2083d44 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/ruby-detect-running-implementation/index.html @@ -15,7 +15,7 @@ 【Ruby】 自身を実行している処理系の種類を判定する|REPL: Rest-Eat-Program Loop - +

      @@ -81,12 +81,13 @@ 上記ページの例から引用する:

      -
      $ 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"
      +
      $ 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 に良い質問と回答があった。 @@ -96,16 +97,17 @@

      -
      | 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 |
      +
      | 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          |
      +

      @@ -121,10 +123,11 @@

      version.h
      -
      /* -
      * Ruby engine. -
      */ -
      #define MRUBY_RUBY_ENGINE "mruby"
      +
      /*
      + * Ruby engine.
      + */
      +#define MRUBY_RUBY_ENGINE  "mruby"
      +
      diff --git a/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html b/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html index 8a13a7e2..ce5b34f0 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/ruby-then-keyword-and-case-in/index.html @@ -15,7 +15,7 @@ 【Ruby】 then キーワードと case in|REPL: Rest-Eat-Program Loop - +
      @@ -103,36 +103,38 @@ 使われることは稀だが、Ruby では then がキーワードになっている。次のように使う:

      -
      if cond then -
      puts "Y" -
      else -
      puts "N" -
      end
      +
      if cond then
      +  puts "Y"
      +else
      +  puts "N"
      +end
      +

      このキーワードが現れうる場所はいくつかあり、ifunlessrescuecase 構文がそれに当たる。 上記のように、何か条件を書いた後 then を置き、式がそこで終了していることを示すマーカーとして機能する。

      -
      # Example: -
      -
      if x then -
      a -
      end -
      -
      unless x then -
      a -
      end -
      -
      begin -
      a -
      rescue then -
      b -
      end -
      -
      case x -
      when p then -
      a -
      end
      +
      # Example:
      +
      +if x then
      +  a
      +end
      +
      +unless x then
      +  a
      +end
      +
      +begin
      +  a
      +rescue then
      +  b
      +end
      +
      +case x
      +when p then
      +  a
      +end
      +
      @@ -141,17 +143,19 @@ 普通 Ruby のコードで then を書くことはない。なぜか。次のコードを実行してみるとわかる。

      -
      if true puts 'Hello, World!' end
      +
      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
      +
      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; か改行が来るはずのところ変数だかメソッドだかが現れたことによりエラーとなっているようだ。 @@ -160,8 +164,9 @@ ポイントは改行が then (や ;) の代わりとなることである。true の後に改行を入れてみる。

      -
      if true -
      puts 'Hello, World!' end
      +
      if true
      +puts 'Hello, World!' end
      +

      無事 Hello, World! と出力されるようになった。 @@ -173,22 +178,25 @@ なぜ then; や改行 (以下 「then 等」) が必要なのだろうか。次の例を見てほしい:

      -
      if a b end
      +
      if a b end
      +

      then; も改行もないのでエラーになるが、これは条件式がどこまで続いているのかわからないためだ。この例は二通りに解釈できる。

      -
      # a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価 -
      if a then -
      b -
      end
      +
      # a という変数かメソッドの評価結果が truthy なら b という変数かメソッドを評価
      +if a then
      +b
      +end
      +
      -
      # a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、 -
      # その結果が truthy なら何もしない -
      if a(b) then -
      end
      +
      # a というメソッドに b という変数かメソッドの評価結果を渡して呼び出し、
      +# その結果が truthy なら何もしない
      +if a(b) then
      +end
      +

      then 等はこの曖昧性を排除するためにあり、条件式は if から then 等までの間にある、ということを明確にする。 C系の if 後に来る (/) や、Python の :、Rust/Go/Swift などの { も同じ役割を持つ。 @@ -209,39 +217,41 @@

      parse.y
      -
      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)) %*/ -
      } -
      ;
      +
      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)) %*/
      +}
      +;
      +

      簡略版:

      -
      p_case_body : keyword_in p_top_expr then compstmt p_cases -
      ;
      +
      p_case_body : keyword_in p_top_expr then compstmt p_cases
      +;
      +

      ここで、keyword_in は文字通り inp_top_expr はいわゆるパターン、thenthen キーワードのことではなく、この記事で then 等と呼んでいるもの、つまり then キーワード、;、改行のいずれかである。 @@ -250,36 +260,38 @@ これにより、case - when による従来の構文と同じように、then 等をパターンの後ろに挿入すればよいことがわかった。つまり次の3通りのいずれかになる:

      -
      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
      +
      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 と似たような見た目になる。

      -
      case x -
      in 0 then a -
      in n if n < 0 then b -
      in n then c -
      end
      +
      case x
      +in 0 then a
      +in n if n < 0 then b
      +in n then c
      +end
      +
      diff --git a/services/nuldoc/public/blog/posts/2021-10-02/rust-where-are-primitive-types-from/index.html b/services/nuldoc/public/blog/posts/2021-10-02/rust-where-are-primitive-types-from/index.html index cf40ebf7..fea2aab4 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/rust-where-are-primitive-types-from/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/rust-where-are-primitive-types-from/index.html @@ -15,7 +15,7 @@ Rust のプリミティブ型はどこからやって来るか|REPL: Rest-Eat-Program Loop - +
      @@ -88,26 +88,27 @@ 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;
      +
      #![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 のソースを追ってみた。 @@ -134,23 +135,25 @@ rustc はセルフホストされている (= rustc 自身が Rust で書かれている) ので、boolchar などで適当に検索をかけてもノイズが多すぎて話にならない。しかし、お誂え向きなことに 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
      +
      $ 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)); -
      ...
      +
      $ git grep "\bi128\b"
      +...
      +rustc_resolve/src/lib.rs:        table.insert(sym::i128, Int(IntTy::I128));
      +...
      +

      rustc_resolve というのはいかにも名前解決を担いそうなクレート名である。該当箇所を見てみる。 @@ -159,72 +162,75 @@

      rustc_resolve/src/lib.rs
      -
      /// 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 } -
      } -
      }
      +
      /// 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.
      +
      All other types are defined somewhere and possibly imported, but the
      +primitive ones need special handling, since they have no place of
      +origin.
      +

      とある。次はこの struct の使用箇所を追う。追うと言っても使われている箇所は次の一箇所しかない。なお説明に不要な箇所は大きく削っている。

      -
      /// 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 -
      }
      +
      /// 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() で作られた変数) に含まれている識別子 (booli32 など) かどうか判定し、そうであればそれに紐づけられたプリミティブ型を返している。 @@ -239,13 +245,14 @@ 動作がわかったところで、例として次のコードを考える。

      -
      #![allow(non_camel_case_types)] -
      -
      struct bool; -
      -
      fn main() { -
      let _: bool = bool; -
      }
      +
      #![allow(non_camel_case_types)]
      +
      +struct bool;
      +
      +fn main() {
      +    let _: bool = bool;
      +}
      +

      ここで main()boolstruct bool として解決される。なぜなら、プリミティブ型の判定をする前に bool という名前の別の型が見つかるからだ。 diff --git a/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html b/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html index 301dcfdc..d3b081f7 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/vim-difference-between-autocmd-bufwrite-and-bufwritepre/index.html @@ -15,7 +15,7 @@ 【Vim】 autocmd events の BufWrite/BufWritePre の違い|REPL: Rest-Eat-Program Loop - +

      @@ -146,8 +146,9 @@
      src/autocmd.c
      -
      {"BufAdd", EVENT_BUFADD}, -
      {"BufCreate", EVENT_BUFADD},
      +
      {"BufAdd",      EVENT_BUFADD},
      +{"BufCreate",   EVENT_BUFADD},
      +

      https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L95-L97 @@ -156,9 +157,10 @@

      src/autocmd.c
      -
      {"BufRead", EVENT_BUFREADPOST}, -
      {"BufReadCmd", EVENT_BUFREADCMD}, -
      {"BufReadPost", EVENT_BUFREADPOST},
      +
      {"BufRead",     EVENT_BUFREADPOST},
      +{"BufReadCmd",  EVENT_BUFREADCMD},
      +{"BufReadPost", EVENT_BUFREADPOST},
      +

      https://github.com/vim/vim/blob/8e6be34338f13a6a625f19bcef82019c9adc65f2/src/autocmd.c#L103-L105 @@ -167,9 +169,10 @@

      src/autocmd.c
      -
      {"BufWrite", EVENT_BUFWRITEPRE}, -
      {"BufWritePost", EVENT_BUFWRITEPOST}, -
      {"BufWritePre", EVENT_BUFWRITEPRE},
      +
      {"BufWrite",    EVENT_BUFWRITEPRE},
      +{"BufWritePost",    EVENT_BUFWRITEPOST},
      +{"BufWritePre", EVENT_BUFWRITEPRE},
      +
      @@ -184,12 +187,13 @@
      src/nvim/auevents.lua
      -
      aliases = { -
      BufCreate = 'BufAdd', -
      BufRead = 'BufReadPost', -
      BufWrite = 'BufWritePre', -
      FileEncoding = 'EncodingChanged', -
      },
      +
      aliases = {
      +  BufCreate = 'BufAdd',
      +  BufRead = 'BufReadPost',
      +  BufWrite = 'BufWritePre',
      +  FileEncoding = 'EncodingChanged',
      +},
      +

      ところで、上では取り上げなかった FileEncoding だが、これは :help FileEncoding にしっかりと書いてある。 @@ -198,9 +202,10 @@

      :help FileEncoding
      -
      *FileEncoding* -
      FileEncoding Obsolete. It still works and is equivalent -
      to |EncodingChanged|.
      +
                                                                    *FileEncoding*
      +FileEncoding                    Obsolete.  It still works and is equivalent
      +                                to |EncodingChanged|.
      +
      diff --git a/services/nuldoc/public/blog/posts/2021-10-02/vim-swap-order-of-selected-lines/index.html b/services/nuldoc/public/blog/posts/2021-10-02/vim-swap-order-of-selected-lines/index.html index 42a946c2..eb1c34c0 100644 --- a/services/nuldoc/public/blog/posts/2021-10-02/vim-swap-order-of-selected-lines/index.html +++ b/services/nuldoc/public/blog/posts/2021-10-02/vim-swap-order-of-selected-lines/index.html @@ -15,7 +15,7 @@ Vimで選択した行の順番を入れ替える|REPL: Rest-Eat-Program Loop - +
      @@ -102,11 +102,12 @@

      TL; DR

      -
      " License: Public Domain -
      -
      command! -bar -range=% -
      \ Reverse -
      \ keeppatterns <line1>,<line2>g/^/m<line1>-1
      +
      " License: Public Domain
      +
      +command! -bar -range=%
      +    \ Reverse
      +    \ keeppatterns <line1>,<line2>g/^/m<line1>-1
      +
      @@ -164,9 +165,10 @@ なお、:g/^/m0 は全ての行を入れ替えるが、:N,Mg/^/mN-1 とすることで N行目から M行目を処理範囲とするよう拡張できる。手でこれを入力するわけにはいかないので、次のようなコマンドを用意する。

      -
      command! -bar -range=% -
      \ Reverse -
      \ <line1>,<line2>g/^/m<line1>-1
      +
      command! -bar -range=%
      +    \ Reverse
      +    \ <line1>,<line2>g/^/m<line1>-1
      +

      これは望みの動作をするが、実際に実行してみると全行がハイライトされてしまう。次節で詳細を述べる。 @@ -200,13 +202,14 @@ 前述した :Reverse コマンドの定義を少し変えて、次のようにする:

      -
      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>)
      +
      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>)
      +

      実行しているコマンドが変わったわけではないが、関数呼び出しを経由するようにした。これだけで前述の問題が解決する。 @@ -253,9 +256,10 @@

      -
      command! -bar -range=% -
      \ Reverse -
      \ keeppatterns <line1>,<line2>g/^/m<line1>-1
      +
      command! -bar -range=%
      +    \ Reverse
      +    \ keeppatterns <line1>,<line2>g/^/m<line1>-1
      +

      まさにこのための Exコマンド、:keeppatterns が存在する。:keeppatterns {command} のように使い、読んで字の如く、後ろに続く Exコマンドを「現在の検索パターンを保ったまま」実行する。はるかに分かりやすく意図を表現できる。 diff --git a/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html b/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html index 7cc11fc6..cfdec959 100644 --- a/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html +++ b/services/nuldoc/public/blog/posts/2022-04-09/phperkaigi-2022-tokens/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2022 トークン問題の解説|REPL: Rest-Eat-Program Loop - +

      @@ -166,75 +166,76 @@
      brainf_ck.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
      +
      +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 で動かせばトークンが得られる。 @@ -259,28 +260,29 @@ なお、brainf*ck プログラムを普通の書き方で書くと、次のようになる。

      -
      + + + + + + + + + + -
      [ -
      > + + + -
      > + + + + + -
      > + + + + + + + + + + + + -
      > + + + + + + + + + + -
      < < < < - -
      ] -
      > + + + + + . -
      - - . -
      > - - - . -
      > - - - . -
      - - . -
      - . -
      < . -
      > > - - . -
      + + + + + + + . -
      < - - - - . -
      < . -
      > + + . -
      > - . -
      < .
      +
      + + + + + + + + + +
      +[
      +  > + + +
      +  > + + + + +
      +  > + + + + + + + + + + + +
      +  > + + + + + + + + + +
      +  < < < < -
      +]
      +> + + + + + .
      +- - .
      +> - - - .
      +> - - - .
      +- - .
      +- .
      +< .
      +> > - - .
      ++ + + + + + + .
      +< - - - - .
      +< .
      +> + + .
      +> - .
      +< .
      +

      実行結果はこちら: https://ideone.com/22VWmb @@ -336,7 +338,8 @@ ソースコードのライセンスを示したこの部分だが、

      -
      https://creativecommons.org/publicdomain/zero/1.0/
      +
      https://creativecommons.org/publicdomain/zero/1.0/
      +

      完全に合法な PHP のコードである。 https: 部分はラベル、// 以降は行コメントになっている。 @@ -348,11 +351,12 @@ ソースコード中に、ほとんど数値リテラルが書かれていないことにお気づきだろうか。PHP では、型変換を利用することで任意の整数を作り出すことができる。

      -
      assert(0 === +!![]); -
      assert(1 === +![]); -
      assert(2 === ![]+![]); -
      assert(3 === ![]+![]+![]); -
      assert(10 === +(![].+!![]));
      +
      assert(0 === +!![]);
      +assert(1 === +![]);
      +assert(2 === ![]+![]);
      +assert(3 === ![]+![]+![]);
      +assert(10 === +(![].+!![]));
      +

      []! を適用すると true が返ってくる。それに + を適用すると、bool から int ヘの型変換が走り、1 が生成される。10 はさらにトリッキーだ。まず 10 を作り、. で文字列として結合する ('10')。これに + を適用すると、string から int への型変換が走り、10 が生まれる (コード量に頓着しないなら、1 を 10 個足し合わせてももちろん 10 が作れる)。 @@ -390,40 +394,41 @@

      riddle.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"; -
      }
      +
      <?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 を特定する必要がある。 @@ -437,33 +442,38 @@ まずはソースコードを読んでいく。

      -
      $token = [ -
      // 略 -
      ];
      +
      $token = [
      +  // 略
      +];
      +

      数値からなる $token があり、各要素をループしている。

      -
      $x = $x ^ N;
      +
      $x = $x ^ N;
      +

      まずは排他的論理和 (xor) を取り、

      -
      $x = sprintf('%025b', $x);
      +
      $x = sprintf('%025b', $x);
      +

      二進数に変換して、

      -
      $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
      +
      $x = str_replace(search: ['0', '1'], replace: [' ', '#'], subject: $x);
      +

      0 を空白に、1 を # にし、

      -
      $x = implode("\n", str_split($x, length: 5));
      +
      $x = implode("\n", str_split($x, length: 5));
      +

      5文字ごとに区切ったあと、改行で結合している。 @@ -501,49 +511,52 @@ N は高々

      -
      assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
      +
      assert(0 <= N && N <= 0b11111_11111_11111_11111_11111);
      +

      なのでブルートフォースしてもよいが、ここではブルートフォースしない方法を紹介する。

      -
      <?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
      +
      +$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 -
      -
      $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";
      +
      <?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 が得られる。 @@ -559,41 +572,43 @@

      toquine.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));
      +
      <?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));
      +

      コメントにもあるとおり、次のようにして実行すれば答えがでてくる。

      -
      $ php toquine.php | php | php | php | ...
      +
      $ php toquine.php | php | php | php | ...
      +

      実際にはもう少しパイプで繋げなければならない。 diff --git a/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html b/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html index 77f4c930..6c5202ed 100644 --- a/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html +++ b/services/nuldoc/public/blog/posts/2022-04-24/term-banner-write-tool-showing-banner-in-terminal/index.html @@ -14,7 +14,7 @@ term-banner: ターミナルにバナーを表示するツールを書いた|REPL: Rest-Eat-Program Loop - +

      @@ -81,7 +81,8 @@ こんなものを作った。

      -
      $ term-banner 'Hello, World!' 'こんにちは、' '世界!'
      +
      $ term-banner 'Hello, World!' 'こんにちは、' '世界!'
      +

      term-banner が動作している様子のスクリーンショット diff --git a/services/nuldoc/public/blog/posts/2022-05-01/phperkaigi-2022/index.html b/services/nuldoc/public/blog/posts/2022-05-01/phperkaigi-2022/index.html index 60346967..1a7578d4 100644 --- a/services/nuldoc/public/blog/posts/2022-05-01/phperkaigi-2022/index.html +++ b/services/nuldoc/public/blog/posts/2022-05-01/phperkaigi-2022/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2022|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html b/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html index e6ac6059..32290405 100644 --- a/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html +++ b/services/nuldoc/public/blog/posts/2022-08-27/php-conference-okinawa-code-golf/index.html @@ -15,7 +15,7 @@ PHP カンファレンス沖縄で出題されたコードゴルフの問題を解いてみた|REPL: Rest-Eat-Program Loop - +
      @@ -144,7 +144,8 @@ 書いたものがこちら:

      -
      [<?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??[]);?>]
      +
      [<?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 バイトとなった (末尾改行を含めずにカウント)。 @@ -153,15 +154,16 @@ こちらは改行とスペースを追加したバージョン:

      -
      [<?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 ?? []); -
      -
      ?>]
      +
      [<?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 ?? []);
      +
      +?>]
      +
      diff --git a/services/nuldoc/public/blog/posts/2022-08-31/support-for-communty-is-employee-benefits/index.html b/services/nuldoc/public/blog/posts/2022-08-31/support-for-communty-is-employee-benefits/index.html index 5fb4ac8d..5e579880 100644 --- a/services/nuldoc/public/blog/posts/2022-08-31/support-for-communty-is-employee-benefits/index.html +++ b/services/nuldoc/public/blog/posts/2022-08-31/support-for-communty-is-employee-benefits/index.html @@ -14,7 +14,7 @@ 弊社の PHP Foundation への寄付に寄せて|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line/index.html b/services/nuldoc/public/blog/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line/index.html index cba40bc8..b49b7096 100644 --- a/services/nuldoc/public/blog/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line/index.html +++ b/services/nuldoc/public/blog/posts/2022-09-29/write-fizzbuzz-in-php-2-letters-per-line/index.html @@ -15,7 +15,7 @@ 【PHP】 fizzbuzz を書く。1行あたり2文字で。|REPL: Rest-Eat-Program Loop - +
      @@ -158,80 +158,81 @@ 特に、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 -
      ); -
      -
      /* あとは同じように普通のプログラムを変形するだけなので省略 */
      +
      #\
      +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
      +);
      +
      +/* あとは同じように普通のプログラムを変形するだけなので省略 */
      +

      バックスラッシュを使った行継続がトークンを区切らない、というのがポイントだ。 @@ -273,10 +274,11 @@ また、2文字だと文字列がまともに書けないのも辛い。'' だけで2文字使うので、「1文字の文字列リテラル」というものを書くことができない。PHP では文字列リテラル中に生の改行が書けるので

      -
      $a -
      =' -
      a' -
      ;;
      +
      $a
      +='
      +a'
      +;;
      +

      とすると $a"\na" になるのだが、余計な改行が入ってしまう。 @@ -293,11 +295,12 @@ まずは普通に書くとしよう。

      -
      <?php -
      -
      for ($i = 1; $i < 100; $i++) { -
      echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"; -
      }
      +
      <?php
      +
      +for ($i = 1; $i < 100; $i++) {
      +  echo (($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n";
      +}
      +

      素直に書いた fizzbuzz とは言い難いが、このくらいは普通だということにしておかないと、この先がやっていられないので許してほしい。 @@ -309,14 +312,15 @@ for は、3文字もある長いキーワードである。こんなものは使えない。array_ 系の関数を使って、適当に置き換えるとしよう。

      -
      <?php -
      -
      $s = range(1, 100); -
      array_walk( -
      $s, -
      fn($i) => -
      printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"), -
      );
      +
      <?php
      +
      +$s = range(1, 100);
      +array_walk(
      +$s,
      +fn($i) =>
      +printf((($i % 3 ? '' : 'Fizz') . ($i % 5 ? '' : 'Buzz') ?: $i) . "\n"),
      +);
      +

      array_walkrangeprintf といった for よりも長いトークンが現れてしまったが、これは次節で直すことにする。なお、echo は文 (statement) であり式 (expression) ではないので、式である printf に置き換えた。 @@ -328,18 +332,19 @@ rangearray_walkprintf は長すぎるのでどうにかせねばならない。ここで、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"), -
      );
      +
      <?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 文字に収めるのか。次のテクニックへ移ろう。 @@ -361,26 +366,28 @@ というルールがない場合、「未定義の定数が評価された場合、その定数の名前が値になる」という PHP 7.x までの仕様が利用できる。例えば、 Fizz という文字列が欲しければ、次のようにする。

      -
      $f -
      =F -
      .i -
      .z -
      .z -
      ;;
      +
      $f
      +=F
      +.i
      +.z
      +.z
      +;;
      +

      こうして簡単に文字列を作れる。なお、この仕様は 7.x 時点でも警告を受けるので、@ 演算子を使って抑制してやるとよい。

      -
      $f -
      =@ -
      F. -
      @i -
      .# -
      @z -
      .# -
      @z -
      ;;
      +
      $f
      +=@
      +F.
      +@i
      +.#
      +@z
      +.#
      +@z
      +;;
      +

      むしろ、このことがわかっていたからこそ PHP 8.x での動作を要件に課したところがある。 @@ -395,66 +402,70 @@ ずばり、文字列同士のビット演算を使う。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
      +
      $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
      +

      これを踏まえ、次のコードを見てみよう。

      -
      $x = "x\nOm\n"; -
      $y = "\nk!\no"; -
      $r = $x ^ $y; -
      echo "$r\n";
      +
      $x = "x\nOm\n";
      +$y = "\nk!\no";
      +$r = $x ^ $y;
      +echo "$r\n";
      +

      実行すると、range が表示される。さて、PHP では文字列リテラル中に生の改行を直接書いてもよいのだった (「主な障害」の節を参照のこと)。書きかえてみよう。

      -
      $x -
      ='x -
      Om -
      '; -
      $y -
      =' -
      k! -
      o' -
      ; -
      -
      $r = $x ^ $y; -
      echo "$r\n";
      +
      $x
      +='x
      +Om
      +';
      +$y
      +='
      +k!
      +o'
      +;
      +
      +$r = $x ^ $y;
      +echo "$r\n";
      +

      さらに # を使って適当に調整すると、次のようになる。

      -
      $x -
      =# -
      'x -
      Om -
      '; -
      $y -
      =' -
      k! -
      o' -
      ;# -
      $r -
      =# -
      $x -
      ^# -
      $y -
      ;# -
      -
      echo "$r\n";
      +
      $x
      +=#
      +'x
      +Om
      +';
      +$y
      +='
      +k!
      +o'
      +;#
      +$r
      +=#
      +$x
      +^#
      +$y
      +;#
      +
      +echo "$r\n";
      +

      1行あたり2文字で、range という文字列を生成することに成功した。他の必要な文字列にも、同様の処理をほどこす。 @@ -470,155 +481,156 @@ 完成したものがこちら。

      -
      <?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 -
      )# -
      .' -
      ') -
      );
      +
      <?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
      +)#
      +.'
      +')
      +);
      +
      @@ -636,18 +648,19 @@ PHP では、バッククォートを使ってシェルを呼び出せる。これは shell_exec 関数と等価である。さて、PHP ではバックスラッシュによる行継続が使えないと書いたが、シェルでは使える (当然だが、呼び出されるシェルに依存する。Bash なら大丈夫だろう。知らんけど)。

      -
      <?php -
      -
      printf(` -
      e\ -
      c\ -
      h\ -
      o\ -
      \ -
      1\ -
      2\ -
      3\ -
      `);
      +
      <?php
      +
      +printf(`
      +e\
      +c\
      +h\
      +o\
      +\
      +1\
      +2\
      +3\
      +`);
      +

      なお、ここでは簡単のため出力に printf をそのまま使っているが、実際には printf という文字列を合成して可変関数で呼び出す。 @@ -669,56 +682,59 @@ もうこれ以上は不可能だと思っていたのだが、この記事の執筆中に解決する方法を思いついたので載せておく。

      -
      <?php -
      -
      $c = 'chr'; -
      -
      ${ -
      '_ -
      '} -
      =# -
      $c -
      (# -
      32 -
      ). -
      $c -
      (# -
      92 -
      ); -
      -
      printf(` -
      e\ -
      c\ -
      h\ -
      o\ -
      ${ -
      '_ -
      '} -
      1\ -
      2\ -
      3\ -
      `);
      +
      <?php
      +
      +$c = 'chr';
      +
      +${
      +'_
      +'}
      +=#
      +$c
      +(#
      +32
      +).
      +$c
      +(#
      +92
      +);
      +
      +printf(`
      +e\
      +c\
      +h\
      +o\
      +${
      +'_
      +'}
      +1\
      +2\
      +3\
      +`);
      +

      先程と同じく、chrprintf を生成する部分は長くなるので省いた。

      -
      ${ -
      '_ -
      '}
      +
      ${
      +'_
      +'}
      +

      は変数で、中にはスペースとエスケープが入っている (chr(32) . chr(92))。シェルに渡されている文字列は次のようになる。

      -
      e\ -
      c\ -
      h\ -
      o\ -
      \ -
      1\ -
      2\ -
      3\
      +
      e\
      +c\
      +h\
      +o\
      +\
      +1\
      +2\
      +3\
      +

      これは、前掲したコマンドと同じだ。かくして、スペースを陽に書かずにシェルをおおよそ自由に扱えるようになった。Fizzbuzz のワンライナーくらいすぐ書けるだろうから、あとはなんとかなるだろう (試してないけど)。 @@ -730,9 +746,10 @@ ちなみに、PHP 8.2 からは、この記法で Warning が出るようになるようだ。

      -
      ${ -
      '_ -
      '}
      +
      ${
      +'_
      +'}
      +

      最新版で警告が出るというのも美しくないので、私としては本編の解法を推す。 diff --git a/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html b/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html index 17fe8449..a0b06a78 100644 --- a/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html +++ b/services/nuldoc/public/blog/posts/2022-10-23/phperkaigi-2023-unused-token-quiz-1/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2023: ボツになったトークン問題 その 1|REPL: Rest-Eat-Program Loop - +

      @@ -102,27 +102,28 @@ 注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。

      -
      <?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"; -
      }
      +
      <?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";
      +}
      +
      @@ -131,15 +132,17 @@ ソースを見るとわかるとおり、$argv[1] を参照している。それを なる変数に代入しているので、円周率を渡してみる。

      -
      $ php Q.php 3.14 -
      Failed.
      +
      $ php Q.php 3.14
      +Failed.
      +

      失敗してしまった。精度を上げてみる。

      -
      $ php Q.php 3.1415 -
      Failed.
      +
      $ php Q.php 3.1415
      +Failed.
      +

      だめだった。これを成功するまで繰り返す。 @@ -148,8 +151,9 @@ 最初にトークンが得られるのは、小数点以下 16 桁目まで入力したときで、こうなる。

      -
      $ php Q.php 3.1415926535897932 -
      Token: #YO
      +
      $ php Q.php 3.1415926535897932
      +Token: #YO
      +

      めでたくトークン「#YO」が手に入った。 @@ -161,20 +165,22 @@ 短いので頭から追っていく。

      -
      = $argv[1] ?? null; -
      if ( === null) { -
      exit('No input.'); -
      } -
      = trim(); -
      if (!is_numeric()) { -
      exit('Invalid input.'); -
      }
      +
       = $argv[1] ?? null;
      +if ( === null) {
      +  exit('No input.');
      +}
      + = trim();
      +if (!is_numeric()) {
      +  exit('Invalid input.');
      +}
      +

      入力のバリデーション部分。数値のみ受け付ける。

      -
      $s = implode(array_map(chr(...), str_split(, 2)));
      +
      $s = implode(array_map(chr(...), str_split(, 2)));
      +

      を 2 文字ごとに区切り (str_split)、数値を ASCII コードと見做して文字に変換 (chr) して結合 (implode) している。 @@ -183,14 +189,16 @@ 例えば、'656667' だったとすると、656667 に対応した 'A''B''C' へと変換され、'ABC' になる。

      -
      = '656667'; -
      $s = implode(array_map(chr(...), str_split(, 2))); -
      echo $s; -
      // => ABC
      +
       = '656667';
      +$s = implode(array_map(chr(...), str_split(, 2)));
      +echo $s;
      +// => ABC
      +
      -
      preg_match('/(\x23.+?) /', $s, $m); -
      $t = $m[1] ?? '';
      +
      preg_match('/(\x23.+?) /', $s, $m);
      +$t = $m[1] ?? '';
      +

      正規表現でマッチングしている。\x23# と同じであることに留意すると、この正規表現は「# から始まる 2 以上の長さ (含 #) の文字列で、最初に現れるスペースまで」にマッチする。つまりこれは、PHPerKaigi におけるトークンである。 @@ -199,11 +207,12 @@ なお、# を直接書いていないのは、/#.+?) / と書くと、#.+?) という意図せぬトークンが登録されてしまうからである。

      -
      if (md5($t) === '056e831a4146bf123e8ea16613303d2e') { -
      echo "Token: {$t}\n"; -
      } else { -
      echo "Failed.\n"; -
      }
      +
      if (md5($t) === '056e831a4146bf123e8ea16613303d2e') {
      +  echo "Token: {$t}\n";
      +} else {
      +  echo "Failed.\n";
      +}
      +

      最後にトークンのハッシュ値を見て、想定解かどうかを確認する。 diff --git a/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html b/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html index 2bd5bb02..8f4dbb75 100644 --- a/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html +++ b/services/nuldoc/public/blog/posts/2022-10-28/setup-server-for-this-site/index.html @@ -15,7 +15,7 @@ 【備忘録】 このサイト用の VPS をセットアップしたときのメモ|REPL: Rest-Eat-Program Loop - +

      @@ -176,8 +176,9 @@ ローカルマシンで鍵を生成する。

      -
      $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/teika.key -
      $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github2teika.key
      +
      $ 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 からサーバへのデプロイ用。 @@ -189,12 +190,13 @@ .ssh/config に設定しておく。

      -
      Host teika -
      HostName ********** -
      User ********** -
      Port ********** -
      IdentityFile ~/.ssh/teika.key -
      IdentitiesOnly yes
      +
      Host teika
      +    HostName **********
      +    User **********
      +    Port **********
      +    IdentityFile ~/.ssh/teika.key
      +    IdentitiesOnly yes
      +
      @@ -212,24 +214,27 @@ 管理者ユーザで作業すると危ないので、メインで使うユーザを作成する。sudo グループに追加して sudo できるようにし、su で切り替え。

      -
      $ sudo adduser ********** -
      $ sudo adduser ********** sudo -
      $ su ********** -
      $ cd
      +
      $ sudo adduser **********
      +$ sudo adduser ********** sudo
      +$ su **********
      +$ cd
      +

      ホスト名を変える

      -
      $ sudo hostname teika
      +
      $ sudo hostname teika
      +

      公開鍵を置く

      -
      $ mkdir ~/.ssh -
      $ chmod 700 ~/.ssh -
      $ vi ~/.ssh/authorized_keys
      +
      $ mkdir ~/.ssh
      +$ chmod 700 ~/.ssh
      +$ vi ~/.ssh/authorized_keys
      +

      authorized_keys には、ローカルで生成した ~/.ssh/teika.key.pub~/.ssh/github2teika.key.pub の内容をコピーする。 @@ -241,8 +246,9 @@ SSH の設定を変更し、少しでも安全にしておく。

      -
      $ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak -
      $ sudo vi /etc/ssh/sshd_config
      +
      $ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
      +$ sudo vi /etc/ssh/sshd_config
      +
      • @@ -259,8 +265,9 @@ そして設定を反映。

        -
        $ sudo systemctl restart sshd -
        $ sudo systemctl status sshd
        +
        $ sudo systemctl restart sshd
        +$ sudo systemctl status sshd
        +
      @@ -269,7 +276,8 @@ 今の SSH セッションは閉じずに、ターミナルを別途開いて疎通確認する。セッションを閉じてしまうと、SSH の設定に不備があった場合に締め出しをくらう。

      -
      $ ssh teika
      +
      $ ssh teika
      +
      @@ -278,11 +286,12 @@ デフォルトの 22 番を閉じ、設定したポートだけ空ける。

      -
      $ sudo ufw deny ssh -
      $ sudo ufw allow ******* -
      $ sudo ufw enable -
      $ sudo ufw reload -
      $ sudo ufw status
      +
      $ sudo ufw deny ssh
      +$ sudo ufw allow *******
      +$ sudo ufw enable
      +$ sudo ufw reload
      +$ sudo ufw status
      +

      ここでもう一度 SSH の接続確認を挟む。 @@ -294,41 +303,46 @@ GitHub に置いてある private リポジトリをサーバから clone したいので、SSH 鍵を生成して置いておく。

      -
      $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github.key -
      $ cat ~/.ssh/github.key.pub
      +
      $ ssh-keygen -t ed25519 -b 521 -f ~/.ssh/github.key
      +$ cat ~/.ssh/github.key.pub
      +

      GitHub の設定画面 から、この公開鍵を追加する。

      -
      $ vi ~/.ssh/config
      +
      $ vi ~/.ssh/config
      +

      設定はこう。

      -
      Host github.com -
      HostName github.com -
      User git -
      Port 22 -
      IdentityFile ~/.ssh/github.key -
      IdentitiesOnly yes
      +
      Host github.com
      +    HostName github.com
      +    User git
      +    Port 22
      +    IdentityFile ~/.ssh/github.key
      +    IdentitiesOnly yes
      +

      最後に接続できるか確認しておく。

      -
      $ ssh -T github.com
      +
      $ ssh -T github.com
      +

      パッケージの更新

      -
      $ sudo apt update -
      $ sudo apt upgrade -
      $ sudo apt update -
      $ sudo apt upgrade -
      $ sudo apt autoremove
      +
      $ sudo apt update
      +$ sudo apt upgrade
      +$ sudo apt update
      +$ sudo apt upgrade
      +$ sudo apt autoremove
      +
      @@ -343,13 +357,15 @@

      使うソフトウェアのインストール

      -
      $ sudo apt install docker docker-compose git make
      +
      $ sudo apt install docker docker-compose git make
      +

      メインユーザが Docker を使えるように

      -
      $ sudo adduser ********** docker
      +
      $ sudo adduser ********** docker
      +
      @@ -358,32 +374,36 @@ 80 番と 443 番を空ける。

      -
      $ sudo ufw allow 80/tcp -
      $ sudo ufw allow 443/tcp -
      $ sudo ufw reload -
      $ sudo ufw status
      +
      $ sudo ufw allow 80/tcp
      +$ sudo ufw allow 443/tcp
      +$ sudo ufw reload
      +$ sudo ufw status
      +

      リポジトリのクローン

      -
      $ cd -
      $ git clone git@github.com:nsfisis/nsfisis.dev.git -
      $ cd nsfisis.dev -
      $ git submodule update --init
      +
      $ cd
      +$ git clone git@github.com:nsfisis/nsfisis.dev.git
      +$ cd nsfisis.dev
      +$ git submodule update --init
      +

      certbot で証明書取得

      -
      $ docker-compose up -d acme-challenge -
      $ make setup
      +
      $ docker-compose up -d acme-challenge
      +$ make setup
      +

      サーバを稼動させる

      -
      $ make serve
      +
      $ make serve
      +
      diff --git a/services/nuldoc/public/blog/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/index.html b/services/nuldoc/public/blog/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/index.html index c3239ed9..b3546e6f 100644 --- a/services/nuldoc/public/blog/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/index.html +++ b/services/nuldoc/public/blog/posts/2022-11-19/phperkaigi-2023-unused-token-quiz-2/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2023: ボツになったトークン問題 その 2|REPL: Rest-Eat-Program Loop - +
      @@ -105,16 +105,17 @@ 注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。

      -
      <?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");?>
      +
      <?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 (自分自身と同じソースコードを出力するプログラム) になっている。 @@ -126,46 +127,49 @@ 実行してみると、次のような出力が得られる。

      -
      # -
      <?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");?>
      +<?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 行目を除き、先ほどのコードとほぼ同じものが出てきた。もう一度実行してみる。

      -
      # -
      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");?>
      +
      #
      +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 行目が書き換えられた。すべての行が変化するまで繰り返すと次のようになる。

      -
      # -
      W -
      E -
      L -
      O -
      V -
      E -
      P -
      H -
      P
      +
      #
      +W
      +E
      +L
      +O
      +V
      +E
      +P
      +H
      +P
      +

      トークン「#WELOVEPHP」が手に入った。 @@ -180,7 +184,8 @@ Vim で開くと次のようになる (1 行目を抜粋)。

      -
      <?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");?>
      +
      <?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 で、ゼロ幅スペースである。 @@ -202,13 +207,15 @@ 続いて、トークンへの変換ロジックを解析する。注目すべきはこの部分だ。以下、ゼロ幅スペースは Vim での表示に合わせて <200b> と記載する。

      -
      fn($s)=>chr(strlen($s)/3)
      +
      fn($s)=>chr(strlen($s)/3)
      +

      PHP の strlen() は文字列のバイト数を返す。1 行目の $s は以下の内容となっており、

      -
      $s='<200b><?php printf((isset($s)?fn($s)=>trim($s,"<200b>"):fn($s)=>chr(strlen($s)/3))($s=%s)."\n","\x27$s\x27");?>'
      +
      $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 コードと一致する。他の行も、同様にしてゼロ幅スペースを詰めることで文字列長を調整し、トークンをエンコードしている。 diff --git a/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html b/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html index 69b1fd47..ef1f779c 100644 --- a/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html +++ b/services/nuldoc/public/blog/posts/2023-01-10/phperkaigi-2023-unused-token-quiz-3/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2023: ボツになったトークン問題 その 3|REPL: Rest-Eat-Program Loop - +

      @@ -121,118 +121,119 @@ 注意: これはボツ問なので、得られたトークンを PHPerKaigi で入力してもポイントにはならない。

      -
      <?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__; -
      }
      +
      <?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==') が得られる。 @@ -266,20 +267,21 @@ このうち 1つ目のケースは、 finally 節の中でエラーを投げると PHP 処理系が勝手に $previous を設定してくれる。

      -
      <?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 -
      }
      +
      <?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
      +}
      +

      この知識を元に、トークンの出力部を解析してみる。 @@ -291,15 +293,16 @@ 出力部をコメントや改行を追加して再掲する:

      -
      <?php -
      try { -
      f(g() / __LINE__); -
      } catch (Throwable $e) { -
      while ($e = $e->getPrevious()) { -
      printf('%c', $e->getLine() + 23); -
      } -
      echo "\n"; -
      }
      +
      <?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 で出力している。 @@ -308,7 +311,8 @@ フォーマット指定子 %c は、整数を ASCII コード[1] と見做して印字する。トークン #base64_decode('SGVsbG8sIFdvcmxkIQ==')b であれば、ASCII コード 98 なので、75 行目で発生したエラー、

      -
      1, 20 => 0 / 0,
      +
      1, 20 => 0 / 0,
      +

      によって表現されている。エラーを起こす方法はいろいろと考えられるが、今回はゼロ除算を使った。 @@ -323,39 +327,42 @@ f() の定義を再掲する (エラーオブジェクトの行数を利用しているので、一部分だけ抜き出すと値が変わることに注意):

      -
      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); -
      } -
      }
      +
      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 -
      try { -
      f(g() / __LINE__); // 3 行目
      +
      <?php
      +try {
      +  f(g() / __LINE__); // 3 行目
      +
      -
      function g() { -
      return __LINE__; // 111 行目 -
      }
      +
      function g() {
      +  return __LINE__; // 111 行目
      +}
      +

      f() には 111 / 337 が渡されることがわかる。そこから 1 ずつ減らして再帰呼び出ししていき、0 より小さくなったら f() を引数なしで呼び出す。引数の数が足りないと呼び出しに失敗するので、再帰はここで止まる。 diff --git a/services/nuldoc/public/blog/posts/2023-03-10/rewrite-this-blog-generator/index.html b/services/nuldoc/public/blog/posts/2023-03-10/rewrite-this-blog-generator/index.html index 544b211d..ab30ffc9 100644 --- a/services/nuldoc/public/blog/posts/2023-03-10/rewrite-this-blog-generator/index.html +++ b/services/nuldoc/public/blog/posts/2023-03-10/rewrite-this-blog-generator/index.html @@ -15,7 +15,7 @@ このブログのジェネレータを書き直した|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html index a099d43c..30c08214 100644 --- a/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html +++ b/services/nuldoc/public/blog/posts/2023-04-01/implementation-of-minimal-png-image-encoder/index.html @@ -14,7 +14,7 @@ PNG 画像の最小構成エンコーダを実装する|REPL: Rest-Eat-Program Loop - +
      @@ -135,44 +135,45 @@ 以下のソースコードをベースにする。今回 PNG のデコーダは扱わないので、読み込みには Go の標準ライブラリ image/png を用いる。

      -
      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) -
      }
      +
      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)
      +}
      +

      以降は、writeSignaturewriteChunkIhdr などを実装していく。 @@ -215,21 +216,22 @@ writeSignature の実装はこちら:

      -
      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) -
      }
      +
      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 バイトを書き込む。 @@ -258,55 +260,57 @@ CRC (Cyclic Redundancy Check) は誤り検出符号の一種。Go 言語では hash/crc32 パッケージにあるが、今回はこれも自前で実装する。PNG の仕様書に C 言語のサンプルコードが載っている ( D. Sample CRC implementation ) ので、これを 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 -
      }
      +
      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 一般を書き込む関数も用意しておこう。

      -
      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)) -
      }
      +
      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))
      +}
      +

      仕様どおり、chunkTypedata から CRC を計算し、data の長さと合わせて書き込んでいる。PNG では基本的に big endian を使うことに注意する。 @@ -384,20 +388,21 @@ 今回ほとんどのデータは決め打ちするので、データに応じて変わるのは width と height だけになる。コードは次のようになる。

      -
      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()) -
      }
      +
      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())
      +}
      +
      @@ -431,22 +436,23 @@ Adler-32 も CRC と同じく誤り検出符号である。こちらも zlib の仕様書に C 言語でサンプルコードが記載されている ( 9. Appendix: Sample code ) ので、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) -
      }
      +
      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 の実装をサボるために使う。 @@ -472,30 +478,31 @@ 実際にこの手抜き zlib を実装したものがこちら:

      -
      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 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()
      +}
      +
      @@ -510,20 +517,21 @@ 先ほどの encodeZlib も使って実際に実装したものがこちら:

      -
      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 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()))
      +}
      +
      @@ -536,9 +544,10 @@ 特に追加のデータはなく、必要なのは chunk type の IEND くらいなので実装は簡単:

      -
      func writeChunkIend(w io.Writer) { -
      writeChunk(w, "IEND", nil) -
      }
      +
      func writeChunkIend(w io.Writer) {
      +	writeChunk(w, "IEND", nil)
      +}
      +
      @@ -548,180 +557,181 @@ 最後に全ソースコードを再掲しておく。

      -
      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) -
      }
      +
      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)
      +}
      +
      diff --git a/services/nuldoc/public/blog/posts/2023-04-04/phperkaigi-2023-report/index.html b/services/nuldoc/public/blog/posts/2023-04-04/phperkaigi-2023-report/index.html index 075b2335..cad5b33d 100644 --- a/services/nuldoc/public/blog/posts/2023-04-04/phperkaigi-2023-report/index.html +++ b/services/nuldoc/public/blog/posts/2023-04-04/phperkaigi-2023-report/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2023 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2023-06-25/phpconfuk-2023-report/index.html b/services/nuldoc/public/blog/posts/2023-06-25/phpconfuk-2023-report/index.html index 58bda47e..6f3b6e8a 100644 --- a/services/nuldoc/public/blog/posts/2023-06-25/phpconfuk-2023-report/index.html +++ b/services/nuldoc/public/blog/posts/2023-06-25/phpconfuk-2023-report/index.html @@ -15,7 +15,7 @@ PHP カンファレンス福岡 2023 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2023-10-02/compile-php-runtime-to-wasm/index.html b/services/nuldoc/public/blog/posts/2023-10-02/compile-php-runtime-to-wasm/index.html index 176793d5..83ab96ca 100644 --- a/services/nuldoc/public/blog/posts/2023-10-02/compile-php-runtime-to-wasm/index.html +++ b/services/nuldoc/public/blog/posts/2023-10-02/compile-php-runtime-to-wasm/index.html @@ -15,7 +15,7 @@ PHP の処理系を Emscripten で WebAssembly にコンパイルする|REPL: Rest-Eat-Program Loop - +
      @@ -142,18 +142,19 @@
      index.mjs
      -
      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}`);
      +
      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) を作成する。 @@ -167,30 +168,31 @@ 先ほどのコードでも使っていたエントリポイントである php_wasm_run を用意する。

      -
      #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; -
      }
      +
      #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点ある。 @@ -213,15 +215,16 @@ デフォルトの出力方法は index.mjs の中で PHPWasm() を呼ぶとき、stdoutstderr というオプションを渡せば変更できる。

      -
      const { ccall } = await PHPWasm({ -
      stdout: (c) => { -
      if (c === null) { -
      // flush the standard output. -
      } else { -
      // output c to the standard output. -
      } -
      }, -
      });
      +
      const { ccall } = await PHPWasm({
      +  stdout: (c) => {
      +    if (c === null) {
      +      // flush the standard output.
      +    } else {
      +      // output c to the standard output.
      +    }
      +  },
      +});
      +

      cnull か 1バイト符号つき整数を取り、null が flush 要求を意味する。 @@ -241,49 +244,52 @@ まずは Emscripten 公式が提供している Docker イメージ を使って、PHP 処理系と先ほど示した C 言語のソースコードを WebAssembly にコンパイルする。

      -
      FROM emscripten/emsdk:3.1.46 AS wasm-builder
      +
      FROM emscripten/emsdk:3.1.46 AS wasm-builder
      +

      次に、 php/php-src から PHP 処理系のソースコードを取得し、ビルドに必要な apt パッケージを取ってくる。有効にする拡張を増やしたいなら、ここでインストールするパッケージも増やすことになるだろう。

      -
      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 \ -
      && \ -
      :
      +
      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 処理系をビルドする。

      -
      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 && \ -
      :
      +
      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 && \
      +    :
      +

      ここまでと比べると少し複雑なので、それぞれ詳しく見ていこう。 @@ -307,22 +313,23 @@ さて、PHP 処理系をライブラリ化できたので、次に先ほど載せた C のソースコードをビルドしていこう。Dockerfile と同じ場所に php-wasm.c という名前で保存し、次のようにする。

      -
      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 && \ -
      :
      +
      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 && \
      +    :
      +

      emcccc (C コンパイラ/リンカ) の Emscripten 版で、-c は「コンパイル」の意。-o-I は普通の C コンパイラと同様、出力ファイルの指定とインクルードパスの指定である。 @@ -331,18 +338,19 @@ libphp.aphp-wasm.o が手に入ったので、これらをリンクして WebAssembly のバイナリとそのラッパである JavaScript ファイルを生成する。これにも emcc コマンドを使う。

      -
      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 \ -
      ;
      +
      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 \
      +    ;
      +

      それぞれのフラグについて解説する。 @@ -375,14 +383,15 @@ といっても、Node.js はビルトインで WebAssembly をサポートしているので、ほとんどやることはない。先ほど掲載した JavaScript のコードは、Dockerfile と同じディレクトリに index.mjs で配置すること。

      -
      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"]
      +
      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"]
      +
      @@ -392,12 +401,13 @@ Dockerfilephp-wasm.cindex.mjs を用意したら、Docker コンテナをビルドして実行する。

      -
      $ docker build -t php-wasm . -
      $ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm -
      Hello, World! -
      -
      -
      exit code: 0
      +
      $ docker build -t php-wasm .
      +$ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm
      +Hello, World!
      +
      +
      +exit code: 0
      +
      diff --git a/services/nuldoc/public/blog/posts/2023-10-13/i-entered-the-open-university-of-japan/index.html b/services/nuldoc/public/blog/posts/2023-10-13/i-entered-the-open-university-of-japan/index.html index 684610a5..b237ce3a 100644 --- a/services/nuldoc/public/blog/posts/2023-10-13/i-entered-the-open-university-of-japan/index.html +++ b/services/nuldoc/public/blog/posts/2023-10-13/i-entered-the-open-university-of-japan/index.html @@ -15,7 +15,7 @@ 放送大学に入学しました|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2023-12-03/isucon-13/index.html b/services/nuldoc/public/blog/posts/2023-12-03/isucon-13/index.html index 837eb08f..8565878f 100644 --- a/services/nuldoc/public/blog/posts/2023-12-03/isucon-13/index.html +++ b/services/nuldoc/public/blog/posts/2023-12-03/isucon-13/index.html @@ -15,7 +15,7 @@ ISUCON 13 に参加した|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2023-12-31/2023-reflections/index.html b/services/nuldoc/public/blog/posts/2023-12-31/2023-reflections/index.html index 49c59338..420ae5b2 100644 --- a/services/nuldoc/public/blog/posts/2023-12-31/2023-reflections/index.html +++ b/services/nuldoc/public/blog/posts/2023-12-31/2023-reflections/index.html @@ -14,7 +14,7 @@ 2023年の振り返り|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file/index.html b/services/nuldoc/public/blog/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file/index.html index bf5ddcea..9e69902a 100644 --- a/services/nuldoc/public/blog/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file/index.html +++ b/services/nuldoc/public/blog/posts/2024-01-10/neovim-insert-namespace-declaration-to-empty-php-file/index.html @@ -15,7 +15,7 @@ 【Neovim】 空の PHP ファイルに namespace 宣言を挿入する|REPL: Rest-Eat-Program Loop - +
      @@ -100,18 +100,20 @@ Neovim で空の PHP ファイルを開いたとき、そのファイルが置かれているディレクトリの構造に基づいて、自動的に namespace 宣言を挿入したい。具体的には、トップレベルの名前空間が MyNamespace であり、ファイル src/Foo/Bar/Baz.php を開いたときに、そのファイルが空であるなら、次のようなテンプレートが自動的に挿入されてほしい。

      -
      <?php -
      -
      namespace MyNamespace\Foo\Bar;
      +
      <?php
      +
      +namespace MyNamespace\Foo\Bar;
      +

      バージョン情報

      -
      $ nvim --version -
      NVIM v0.9.2 -
      Build type: Release -
      LuaJIT 2.1.1693350652
      +
      $ nvim --version
      +NVIM v0.9.2
      +Build type: Release
      +LuaJIT 2.1.1693350652
      +

      今回は Lua で処理を記述したため、Vim では動作しない。以下の説明でも Neovim に絞って述べる。また、パス区切りがスラッシュである前提で記述したため、Windows には対応していない。 @@ -135,13 +137,14 @@ ファイルタイプは読み込んだあとに変更されることもあるので、ftplugin は複数回実行されうる。二重読み込みを防ぐために、did_ftplugin_<FILE_TYPE>_after というバッファローカル変数を定義しておくのが慣習となっている。

      -
      if vim.b.did_ftplugin_php_after then -
      return -
      end -
      -
      -- ここに実際の処理を書く -
      -
      vim.b.did_ftplugin_php_after = true
      +
      if vim.b.did_ftplugin_php_after then
      +   return
      +end
      +
      +-- ここに実際の処理を書く
      +
      +vim.b.did_ftplugin_php_after = true
      +
      @@ -173,119 +176,120 @@ 実装を簡単にするため、Composer を用いない場合や PSR 4 以外のオートロード規則を使う場合には対応しない。少々長くなるが、以下にスクリプト全文を載せる。

      -
      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
      +
      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
      +
      diff --git a/services/nuldoc/public/blog/posts/2024-02-03/install-wireguard-on-personal-server/index.html b/services/nuldoc/public/blog/posts/2024-02-03/install-wireguard-on-personal-server/index.html index 7b4db912..f05bfda3 100644 --- a/services/nuldoc/public/blog/posts/2024-02-03/install-wireguard-on-personal-server/index.html +++ b/services/nuldoc/public/blog/posts/2024-02-03/install-wireguard-on-personal-server/index.html @@ -15,7 +15,7 @@ 【備忘録】 個人用サーバに WireGuard を導入する|REPL: Rest-Eat-Program Loop - +
      @@ -119,14 +119,16 @@ まずは個人用サービスをホストしている Ubuntu のサーバに WireGuard をインストールする。

      -
      $ sudo apt install 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}
      +
      $ wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub
      +$ sudo chmod 600 /etc/wireguard/server.{key,pub}
      +
      @@ -135,26 +137,28 @@ 公式サイトから各 OS 向けのクライアントソフトウェアを入手し、インストールする。次に、設定をおこなう。

      -
      # クライアント 1 の場合 -
      [Interface] -
      Address = 10.10.1.2/32 -
      PrivateKey = <クライアント 1 の秘密鍵> -
      -
      [Peer] -
      PublicKey = <サーバの公開鍵> -
      AllowedIPs = <サーバの外部 IP アドレス>/32 -
      Endpoint = <サーバの外部 IP アドレス>:51820
      +
      # クライアント 1 の場合
      +[Interface]
      +Address = 10.10.1.2/32
      +PrivateKey = <クライアント 1 の秘密鍵>
      +
      +[Peer]
      +PublicKey = <サーバの公開鍵>
      +AllowedIPs = <サーバの外部 IP アドレス>/32
      +Endpoint = <サーバの外部 IP アドレス>:51820
      +
      -
      # クライアント 2 の場合 -
      [Interface] -
      Address = 10.10.1.3/32 -
      PrivateKey = <クライアント 2 の秘密鍵> -
      -
      [Peer] -
      PublicKey = <サーバの公開鍵> -
      AllowedIPs = <サーバの外部 IP アドレス>/32 -
      Endpoint = <サーバの外部 IP アドレス>:51820
      +
      # クライアント 2 の場合
      +[Interface]
      +Address = 10.10.1.3/32
      +PrivateKey = <クライアント 2 の秘密鍵>
      +
      +[Peer]
      +PublicKey = <サーバの公開鍵>
      +AllowedIPs = <サーバの外部 IP アドレス>/32
      +Endpoint = <サーバの外部 IP アドレス>:51820
      +

      PrivateKeyPublicKey は鍵ファイルのパスではなく中身を書くことに注意。 @@ -166,29 +170,32 @@ 一度サーバへ戻り、WireGuard の設定ファイルを書く。

      -
      $ sudo vim /etc/wireguard/wg0.conf
      +
      $ sudo vim /etc/wireguard/wg0.conf
      +
      -
      [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
      +
      [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
      +
      $ sudo systemctl enable wg-quick@wg0
      +$ sudo systemctl start wg-quick@wg0
      +
      @@ -197,23 +204,26 @@ 続けてファイアウォールを設定する。まずは WireGuard が使用する UDP のポートを開き、wg0 を通る通信を許可する。

      -
      $ sudo ufw allow 51820/udp -
      $ sudo ufw allow in on wg0 -
      $ sudo ufw allow out on 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
      +
      $ 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
      +
      $ sudo ufw status
      +$ sudo ufw enable
      +
      diff --git a/services/nuldoc/public/blog/posts/2024-02-10/yapcjapan-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-02-10/yapcjapan-2024-report/index.html index 79f26755..2ad23c13 100644 --- a/services/nuldoc/public/blog/posts/2024-02-10/yapcjapan-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-02-10/yapcjapan-2024-report/index.html @@ -15,7 +15,7 @@ YAPC::Hiroshima 2024 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-02-22/phpkansai-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-02-22/phpkansai-2024-report/index.html index 1214307a..eb952662 100644 --- a/services/nuldoc/public/blog/posts/2024-02-22/phpkansai-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-02-22/phpkansai-2024-report/index.html @@ -15,7 +15,7 @@ PHPカンファレンス関西 2024 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-03-17/phperkaigi-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-03-17/phperkaigi-2024-report/index.html index 8b2a793b..9e88f411 100644 --- a/services/nuldoc/public/blog/posts/2024-03-17/phperkaigi-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-03-17/phperkaigi-2024-report/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2024 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-03-20/my-bucket-list/index.html b/services/nuldoc/public/blog/posts/2024-03-20/my-bucket-list/index.html index 1571b2f3..036337c5 100644 --- a/services/nuldoc/public/blog/posts/2024-03-20/my-bucket-list/index.html +++ b/services/nuldoc/public/blog/posts/2024-03-20/my-bucket-list/index.html @@ -14,7 +14,7 @@ 死ぬまでに作る自作○○一覧あるいは人生の TODO リスト|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-04-14/phpcon-odawara-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-04-14/phpcon-odawara-2024-report/index.html index 536f0347..6cc2c36e 100644 --- a/services/nuldoc/public/blog/posts/2024-04-14/phpcon-odawara-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-04-14/phpcon-odawara-2024-report/index.html @@ -15,7 +15,7 @@ PHP カンファレンス小田原 2024 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd/index.html b/services/nuldoc/public/blog/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd/index.html index 27efd926..40d64edc 100644 --- a/services/nuldoc/public/blog/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd/index.html +++ b/services/nuldoc/public/blog/posts/2024-04-21/pipefail-option-in-gitlab-ci-cd/index.html @@ -15,7 +15,7 @@ 【GitLab】 GitLab CI/CD 上での bash/sh は pipefail が有効になっている|REPL: Rest-Eat-Program Loop - +
      @@ -116,14 +116,15 @@ 例:

      -
      hello-world: -
      stage: test -
      image: alpine:latest -
      script: -
      - 'echo "Hello, World!"' -
      rules: -
      - if: '$CI_MERGE_REQUEST_IID' -
      when: always
      +
      hello-world:
      +  stage: test
      +  image: alpine:latest
      +  script:
      +    - 'echo "Hello, World!"'
      +  rules:
      +    - if: '$CI_MERGE_REQUEST_IID'
      +  when: always
      +

      ここで、script に指定したコマンドが失敗する (exit status が 0 以外になる) と、即座に実行が停止され、ジョブは失敗する。 @@ -132,14 +133,15 @@ では、次のようなケースだとどうなるか。

      -
      hello-world: -
      stage: test -
      image: alpine:latest -
      script: -
      - 'exit 1 | exit 0' -
      rules: -
      - if: '$CI_MERGE_REQUEST_IID' -
      when: always
      +
      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 になる。 @@ -151,10 +153,11 @@ 前述したようなケースにおいて、途中で失敗したときに全体を失敗させるには、pipefail オプションを有効にする。

      -
      # On にする -
      set -o pipefail -
      # Off にする -
      set +o pipefail
      +
      # On にする
      +set -o pipefail
      +# Off にする
      +set +o pipefail
      +

      こうすると、パイプ全体が失敗するようになる。この設定は、デフォルトだと off になっている。 @@ -167,14 +170,15 @@ 次のような GitLab CI/CD ジョブが失敗してしまった。

      -
      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
      +
      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 になっていた。 @@ -183,20 +187,21 @@ しかし、先述したように 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
      +
      $ 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 は無効になっている。 @@ -211,9 +216,10 @@ .gitlab-ci.yml で明示的には書いていないので、GitLab Runner (GitLab CI/CD のスクリプトを実行するプログラム) が勝手に追加しているに違いない。そう仮説を立てて GitLab Runner のリポジトリ を調査したところ、 ソースコード中の以下の箇所set -o pipefail していることが判明した (コメントは筆者による)。

      -
      // pipefail オプションが存在しない環境にも対応するため、 -
      // 先に set -o でオプション一覧を表示させたあと、set -o pipefail している -
      buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
      +
      // pipefail オプションが存在しない環境にも対応するため、
      +// 先に set -o でオプション一覧を表示させたあと、set -o pipefail している
      +buf.WriteString("if set -o | grep pipefail > /dev/null; then set -o pipefail; fi; set -o errexit\n")
      +
      @@ -222,16 +228,17 @@ 通常の Bash スクリプトを書く場合と同様に、pipefail が on になっていては困る場所だけ off にしてやればよい。

      -
      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
      +
       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
      +
      diff --git a/services/nuldoc/public/blog/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands/index.html b/services/nuldoc/public/blog/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands/index.html index e252c345..dfa6dea2 100644 --- a/services/nuldoc/public/blog/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands/index.html +++ b/services/nuldoc/public/blog/posts/2024-04-29/zsh-file-completion-for-composer-custom-commands/index.html @@ -15,7 +15,7 @@ 【Zsh】 Composer のカスタムコマンドに対する Zsh 補完で引数にファイルを補完させる|REPL: Rest-Eat-Program Loop - +
      @@ -116,11 +116,12 @@ このことは、先ほどリンクを載せた _composer 関数を定義しているファイルの冒頭にも書かれている。

      -
      # - @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
      +
      # - @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
      +
      @@ -138,7 +139,8 @@ まずは、Zsh で補完関数を提供する場合のボイラープレートコードを書く。以下は ~/.zshrc にすべて書く前提だが、autoload を設定するなどすれば別ファイルに分離できる (詳細な手順は割愛)。

      -
      compdef _my_composer composer composer.phar
      +
      compdef _my_composer composer composer.phar
      +

      compdef は Zsh が用意している関数で、第一引数に補完関数の名前、第二引数以降に補完を適用するコマンド名を並べる。この場合は、composer コマンドや composer.phar コマンドに対して _my_composer を使って補完をおこなうよう定義している。 @@ -147,9 +149,10 @@ 次に _my_composer を定義する。基本的にはデフォルトの composer コマンドの補完関数 (つまり _composer 関数) を使い、それが何も返さなかった場合に限り、Zsh のファイル・ディレクトリ補完へフォールバックする。

      -
      function _my_composer() { -
      _composer "$@" || _files "$@" -
      }
      +
      function _my_composer() {
      +    _composer "$@" || _files "$@"
      +}
      +

      _composer コマンドは何も補完候補がなかったとき非ゼロな exit status で終了するので、そうであったなら _files を呼び出す。_files は、Zsh がデフォルトで用意しているファイル・ディレクトリの補完をおこなう関数である。 diff --git a/services/nuldoc/public/blog/posts/2024-05-11/phpconkagawa-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-05-11/phpconkagawa-2024-report/index.html index 71a34276..ce98d0ff 100644 --- a/services/nuldoc/public/blog/posts/2024-05-11/phpconkagawa-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-05-11/phpconkagawa-2024-report/index.html @@ -15,7 +15,7 @@ PHP カンファレンス香川 2024 参加レポ|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2024-06-19/scalamatsuri-2024-report/index.html b/services/nuldoc/public/blog/posts/2024-06-19/scalamatsuri-2024-report/index.html index 274af9ba..0faf054d 100644 --- a/services/nuldoc/public/blog/posts/2024-06-19/scalamatsuri-2024-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-06-19/scalamatsuri-2024-report/index.html @@ -15,7 +15,7 @@ ScalaMatsuri 2024 参加レポ|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html b/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html index 4afef2e5..42bbb29b 100644 --- a/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html +++ b/services/nuldoc/public/blog/posts/2024-07-19/reparojson-fix-only-json-formatter/index.html @@ -15,7 +15,7 @@ reparojson: 文法エラーを直すだけの JSON フォーマッタを作った|REPL: Rest-Eat-Program Loop - +
      @@ -106,17 +106,18 @@ 次のように動作する。

      -
      $ 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 }
      +
      $ 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 時点で修正対象の文法エラーは次のとおり: @@ -148,33 +149,34 @@ ここでは、 nvim-lspconfigefm-langserver を用いた設定例を紹介する。

      -
      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, -
      })
      +
         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 で終了するようにしている。 @@ -186,28 +188,31 @@ このツールが威力を発揮するのは、行の入れ換え時である。次のような JSON があり、

      -
      { -
      "a": true, -
      "b": false -
      }
      +
         {
      +      "a": true,
      +      "b": false
      +   }
      +

      2行目と3行目を入れ換えて以下のように編集した。

      -
      { -
      "b": false -
      "a": true, -
      }
      +
         {
      +      "b": false
      +      "a": true,
      +   }
      +

      これは不正な JSON だが、このツールを通せば次のようになる。

      -
      { -
      "b": false, -
      "a": true -
      }
      +
         {
      +      "b": false,
      +      "a": true
      +   }
      +

      もちろん、このような操作を文法を壊さずにおこなう Vim プラグインは存在する。しかし、単なる行の入れ換えであれば ddp の3ストロークでおこなうことができ、専用のキーバインドを覚える必要もない。このツールを用いることで、より Vimmer-friendly な JSON 編集が可能となる。 diff --git a/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html b/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html index 987c0f0d..b9496c6e 100644 --- a/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html +++ b/services/nuldoc/public/blog/posts/2024-08-19/go-template-access-outer-scope-pipeline-within-with-or-range/index.html @@ -15,7 +15,7 @@ 【Go】 text/template の with や range の内側から外側の "." にアクセスする|REPL: Rest-Eat-Program Loop - +

      @@ -90,19 +90,20 @@ Go には、標準ライブラリにテンプレートライブラリ text/template がある。この text/template における制御構造、withrange は次のように使われる。

      -
      # {{ .Title }} -
      -
      # User -
      -
      {{ with .User }} -
      {{ .Name }} ({{ .ID }}) -
      {{ end }} -
      -
      # Items -
      -
      {{ range .Items }} -
      - {{ . }} -
      {{ end }}
      +
      # {{ .Title }}
      +
      +# User
      +
      +{{ with .User }}
      +  {{ .Name }} ({{ .ID }})
      +{{ end }}
      +
      +# Items
      +
      +{{ range .Items }}
      +  - {{ . }}
      +{{ end }}
      +

      text/template. は、現在の操作対象を表す特殊なオブジェクトである。 @@ -114,18 +115,19 @@ つまりこのテンプレートは、次のような構造をレンダリングしている (Execute() の第2引数)。

      -
      tmpl.Execute(out, Params{ -
      Title: "foo", -
      User: User{ -
      ID: 123, -
      Name: "john", -
      }, -
      Items: []string{ -
      "hoge", -
      "piyo", -
      "fuga", -
      }, -
      })
      +
      tmpl.Execute(out, Params{
      +    Title: "foo",
      +    User: User{
      +        ID:   123,
      +        Name: "john",
      +    },
      +    Items: []string{
      +        "hoge",
      +        "piyo",
      +        "fuga",
      +    },
      +})
      +
      @@ -134,13 +136,14 @@ 今回おこないたいのは、withrange の中で、その外側で使われていたトップレベルのオブジェクトを参照することだ。

      -
      {{ with .User }} -
      ここから .Title を参照するには? -
      {{ end }} -
      -
      {{ range .Items }} -
      ここから .User を参照するには? -
      {{ end }}
      +
      {{ with .User }}
      +  ここから .Title を参照するには?
      +{{ end }}
      +
      +{{ range .Items }}
      +  ここから .User を参照するには?
      +{{ end }}
      +

      withrange は、. を自身の対象オブジェクトに変更するので、単に {{ with .User }} の中で .Title と書いても、それは UserTitle プロパティを参照しているとみなされる。 @@ -149,7 +152,8 @@ text/template では変数が使えるので、テンプレートの先頭で

      -
      {{ $params := . }}
      +
      {{ $params := . }}
      +

      とでもしておけば実現は可能である。 @@ -164,13 +168,14 @@ 常にトップレベルを指す特殊変数 $ を使えばよい。

      -
      {{ with .User }} -
      {{ $.Title }} -
      {{ end }} -
      -
      {{ range .Items }} -
      {{ $.User.Name }} -
      {{ end }}
      +
      {{ with .User }}
      +  {{ $.Title }}
      +{{ end }}
      +
      +{{ range .Items }}
      +  {{ $.User.Name }}
      +{{ end }}
      +

      $ は、テンプレートが実行されるときに渡されたオブジェクトを指す。これを使えば現在の . に関係なくトップレベルを参照できる。 diff --git a/services/nuldoc/public/blog/posts/2024-09-28/mncore-challenge-1/index.html b/services/nuldoc/public/blog/posts/2024-09-28/mncore-challenge-1/index.html index e1fbc335..e0be9560 100644 --- a/services/nuldoc/public/blog/posts/2024-09-28/mncore-challenge-1/index.html +++ b/services/nuldoc/public/blog/posts/2024-09-28/mncore-challenge-1/index.html @@ -15,7 +15,7 @@ MN-Core Challenge #1 参加レポ|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html b/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html index 74e77e2b..c8825653 100644 --- a/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html +++ b/services/nuldoc/public/blog/posts/2024-12-04/cohackpp-report/index.html @@ -15,7 +15,7 @@ 紅白ぺぱ合戦に参加&LTしました|REPL: Rest-Eat-Program Loop - +
      @@ -157,129 +157,130 @@ https://github.com/nsfisis/cohackpp/blob/main/congrats.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));
      +
      <?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/nuldoc/public/blog/posts/2024-12-33/2024-reflections/index.html b/services/nuldoc/public/blog/posts/2024-12-33/2024-reflections/index.html index 4e73b518..38f71bab 100644 --- a/services/nuldoc/public/blog/posts/2024-12-33/2024-reflections/index.html +++ b/services/nuldoc/public/blog/posts/2024-12-33/2024-reflections/index.html @@ -14,7 +14,7 @@ 2024年の振り返り|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2025-01-08/phperkaigi-2023-tokens-q1/index.html b/services/nuldoc/public/blog/posts/2025-01-08/phperkaigi-2023-tokens-q1/index.html index 069f1fbd..535064a0 100644 --- a/services/nuldoc/public/blog/posts/2025-01-08/phperkaigi-2023-tokens-q1/index.html +++ b/services/nuldoc/public/blog/posts/2025-01-08/phperkaigi-2023-tokens-q1/index.html @@ -15,7 +15,7 @@ PHPerKaigi 2023 トークン問題解説 (1/5)|REPL: Rest-Eat-Program Loop - +
      @@ -167,7 +167,8 @@ まずはトークンを得る方法を解説抜きで説明する。次のように実行する。

      -
      $ echo "#iwillblog" | php Q1.png >/dev/null
      +
      $ echo "#iwillblog" | php Q1.png >/dev/null
      +

      無事に実行できていれば「#ModernPHPisStaticallyTypedLanguage」というトークンが得られる。 @@ -181,7 +182,8 @@ まずは素直に画像として見てみよう。全体は QR コードになっている。適当な QR コードリーダで読み込むと、次のようなテキストが表示されるはずだ。

      -
      Guess password. $ echo "password" | php Q1.png >/dev/null
      +
      Guess password. $ echo "password" | php Q1.png >/dev/null
      +

      メッセージは、この画像の実行方法とこの問題でやるべきこと (パスワードの推測) を示している。 @@ -196,8 +198,9 @@ 不正なパスワードを使って実行してみると、次のようなエラーメッセージが表示される。

      -
      $ echo "foo" | php Q1.png >/dev/null -
      401 Unauthorized
      +
      $ echo "foo" | php Q1.png >/dev/null
      +401 Unauthorized
      +

      すでに 「解き方」の節 で示したように、パスワードである PHPer トークンは「#iwillblog」である。これを与えて実行すると正解のトークンが得られる。 @@ -267,23 +270,25 @@ 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
      +-HHc
      +<PLTE
      +IDATx
      +IEND
      +<?php
      +error_reporting(-1);
      +$b = unpack('C*', file_get_contents(__FILE__));
      +$w = $b[20]+2;
      +$h = $b[24]+2;
      +// (以下略)
      +

      IHDRIEND が PNG 画像の一部で、<?php からが実際のプログラムになっている。もちろんこれを PHP プログラムとして動かすと、PHP タグより前にある PNG 画像としてのデータはそのまま標準出力へと出力されてしまう。それを防ぐため、QR コードを読み込んだときの実行方法

      -
      Guess password. $ echo "password" | php Q1.png >/dev/null
      +
      Guess password. $ echo "password" | php Q1.png >/dev/null
      +

      には標準出力を捨てるよう >/dev/null と指定されている。 @@ -298,107 +303,108 @@ 画像の正体がわかったところで、画像に隠されていた 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));
      +
      <?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」 などを参照してほしい。 @@ -407,7 +413,8 @@ プログラムの冒頭にあるこの箇所

      -
      $b = unpack('C*', file_get_contents(__FILE__));
      +
      $b = unpack('C*', file_get_contents(__FILE__));
      +

      __FILE__ つまりこの画像ファイルを読み込んでいる。先ほど Piet は画像をソースコードにしていると説明した。そう、今回の問題の画像ファイル Q1.png は、PHP 製 Piet インタプリタであると同時に、Piet のソースコード画像でもあるのだ。QR コード中央のカラフルな部分が Piet の命令になっている。 @@ -459,11 +466,12 @@ ところで、先ほど掲載した Piet のインタプリタのソースコード末尾には次のような箇所がある。

      -
      // 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));
      +
      // 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 プログラマならできるかもしれないので挑戦してみてほしい)。 diff --git a/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html b/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html index a6c44c2a..da6dde78 100644 --- a/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html +++ b/services/nuldoc/public/blog/posts/2025-01-26/yaml-breaking-changes-between-v1-1-and-v1-2/index.html @@ -15,7 +15,7 @@ 【YAML】YAML 1.1 と YAML 1.2 の主な破壊的変更|REPL: Rest-Eat-Program Loop - +

      @@ -128,14 +128,15 @@ YAML 1.1 では、<< という文字列をキーに指定することで、マップをマージすることができた。

      -
      x: &base -
      a: 123 -
      # => { "x": { "a": 123 } } -
      -
      y: -
      <<: *base -
      b: 456 -
      # => { "y": { "a": 123, "b": 456 } }
      +
      x: &base
      +  a: 123
      +# => { "x": { "a": 123 } }
      +
      +y:
      +  <<: *base
      +  b: 456
      +# => { "y": { "a": 123, "b": 456 } }
      +

      1.2 からはこれができなくなる。 diff --git a/services/nuldoc/public/blog/posts/2025-02-24/phpcon-nagoya-2025-report/index.html b/services/nuldoc/public/blog/posts/2025-02-24/phpcon-nagoya-2025-report/index.html index e0a5889c..a09d1851 100644 --- a/services/nuldoc/public/blog/posts/2025-02-24/phpcon-nagoya-2025-report/index.html +++ b/services/nuldoc/public/blog/posts/2025-02-24/phpcon-nagoya-2025-report/index.html @@ -15,7 +15,7 @@ PHP カンファレンス名古屋 2025 参加レポ|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html b/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html index 8a95b643..7968b9ec 100644 --- a/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html +++ b/services/nuldoc/public/blog/posts/2025-03-27/zip-function-like-command-paste-command/index.html @@ -15,7 +15,7 @@ zip 関数のようなコマンド paste|REPL: Rest-Eat-Program Loop - +
      @@ -80,28 +80,31 @@ a.txt

      -
      a1 -
      a2 -
      a3
      +
      a1
      +a2
      +a3
      +

      b.txt

      -
      b1 -
      b2 -
      b3
      +
      b1
      +b2
      +b3
      +

      ab.txt

      -
      a1 -
      b1 -
      a2 -
      b2 -
      a3 -
      b3
      +
      a1
      +b1
      +a2
      +b2
      +a3
      +b3
      +

      ちょうど Python や Haskell などにある zip 関数のような動きをさせたい。 @@ -113,8 +116,9 @@ 記事タイトルに書いたように、paste コマンドを使うと実現できる。

      -
      $ paste -d '\ -
      ' a.txt b.txt > ab.txt
      +
      $ paste -d '\
      +' a.txt b.txt > ab.txt
      +

      paste コマンドは複数のファイルを引数に取り、それらを1行ずつ消費しながら -d で指定した文字で区切って出力する。-d は区切り文字の指定で、デフォルトだとタブ区切りになる。 @@ -123,22 +127,24 @@ ファイル名には - を指定でき、その場合は標準入力から読み込んで出力する。このとき paste - - のように複数回 - を指定すると、指定した回数の行ごとに連結することができる。例えば ab.txt だとこうなる。

      -
      $ paste - - < ab.txt -
      a1 b1 -
      a2 b2 -
      a3 b3
      +
      $ 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
      +
      $ paste ab.txt ab.txt
      +a1	a1
      +b1	b1
      +a2	a2
      +b2	b2
      +a3	a3
      +b3	b3
      +

      ときどき便利。 diff --git a/services/nuldoc/public/blog/posts/2025-03-28/http-1-1-send-multiple-same-headers/index.html b/services/nuldoc/public/blog/posts/2025-03-28/http-1-1-send-multiple-same-headers/index.html index aca24bb5..40cf7ef7 100644 --- a/services/nuldoc/public/blog/posts/2025-03-28/http-1-1-send-multiple-same-headers/index.html +++ b/services/nuldoc/public/blog/posts/2025-03-28/http-1-1-send-multiple-same-headers/index.html @@ -15,7 +15,7 @@ 【HTTP】HTTP/1.1 で同じヘッダを2回送るとどうなるか|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html index 1e161d0f..e1948dc4 100644 --- a/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html +++ b/services/nuldoc/public/blog/posts/2025-04-20/trick-2025-most-ruby-on-ruby-award/index.html @@ -15,7 +15,7 @@ RubyKaigi 2025 の TRICK で入賞した|REPL: Rest-Eat-Program Loop - +
      @@ -161,7 +161,8 @@ 表示している。つまり、Ruby プログラムにルビを振った作品である。例えば、先頭の2行目の require は次のような HTML で構成されている。

      -
      <ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby>
      +
      <ruby class="IDENTIFIER">require<rp class="">(</rp><rt class="">リクワイア</rt><rp class="">)</rp></ruby>
      +

      順に使ったテクニックを解説していく。 @@ -172,11 +173,12 @@ 改めて quine について説明する。Quine とは、自身のソースコードを出力するようなプログラムである。Ruby では様々な方法で quine を書くことができるが、この作品で使っている基本形は以下のようなものである。

      -
      eval $s=<<'EOS' -
      print "eval $s=<<'EOS'\n" -
      print $s -
      print "EOS\n" -
      EOS
      +
      eval $s=<<'EOS'
      +print "eval $s=<<'EOS'\n"
      +print $s
      +print "EOS\n"
      +EOS
      +

      変数 $s に 2 行目、3 行目、4 行目が入っており、それに加えて 1 行目と 5 行目を出力すれば元のソースコードが得られる。実際には $s を加工してシンタックスハイライトや振り仮名を振ることになる。 @@ -191,23 +193,24 @@ トークナイズには Ruby 3.4 からデフォルトのパーサになった Prism を利用している。Prism.lex() を使うとトークナイズができるので、トークンに付いているソースコード位置の情報を使いつつ元のソースコードを復元する。

      -
      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 # 現在列を更新 -
      }
      +
      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    # 現在列を更新
      +}
      +

      補足: 変数名がやたら短いのは、このあとの振り仮名データの量を削減するため。 @@ -216,20 +219,21 @@ トークン種別に応じた色付けは CSS でおこなっている。出力する HTML のクラス名に Prism::Token#type を指定しておいて、index.html でそれぞれのクラスにスタイルを当てた。

      -
      <style> -
      /* ... */ -
      -
      .COMMENT { -
      color: #777; -
      font-style: italic; -
      } -
      -
      .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER { -
      color: #088; -
      } -
      -
      /* ... */ -
      </style>
      +
          <style>
      +      /* ... */
      +
      +      .COMMENT {
      +        color: #777;
      +        font-style: italic;
      +      }
      +
      +      .CONSTANT, .GLOBAL_VARIABLE, .INSTANCE_VARIABLE, .IDENTIFIER {
      +        color: #088;
      +      }
      +
      +      /* ... */
      +    </style>
      +

      トークン種別の列挙にはそれなりに文字数を使ってしまうのだが、今回の TRICK のレギュレーションでは index.html にサイズ制限がなかったので好きに色を付けることができた。 @@ -241,27 +245,28 @@ それぞれの英単語や記号に対応した振り仮名のデータは、プログラム中に埋め込まれている。

      -
      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
      +
      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")。 @@ -270,25 +275,27 @@ このテーブルはサイズ制限を突破するために圧縮されており、kana() 関数で展開される。

      -
      def kana(s) -
      s -
      &.scan(/.{2}/) -
      &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)} -
      &.*("") -
      end
      +
      def kana(s)
      +  s
      +    &.scan(/.{2}/)
      +    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
      +    &.*("")
      +end
      +

      例えば value に対応する振り仮名データ "48746992" であれば、次のような変換を経て振り仮名へと展開される。

      -
      s -
      # => "48746992" -
      &.scan(/.{2}/) -
      # => ["48", "74", "69", "92"] -
      &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)} -
      # => ["バ", "リ", "ュ", "ー"] -
      &.*("") -
      # => "バリュー"
      +
        s
      +    # => "48746992"
      +    &.scan(/.{2}/)
      +    # => ["48", "74", "69", "92"]
      +    &.map{|c| (0x30A0 + c.to_i).chr(Encoding::UTF_8)}
      +    # => ["バ", "リ", "ュ", "ー"]
      +    &.*("")
      +    # => "バリュー"
      +

      これは後で気付いたのだが、Ruby は多倍長整数が扱えるので "48746992" のようなデータは単に 48746992 と書けばよかった。kana() 関数が多少長くはなるが、振り仮名データの数 x 2 バイト分サイズが減るのでこちらの方が短くなる。サイズ制限の都合で振り仮名を振るのを諦めた記号もあったのでもったいない。 diff --git a/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html b/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html index 7e979500..6cad6654 100644 --- a/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html +++ b/services/nuldoc/public/blog/posts/2025-04-24/composer-patches-v2-does-not-require-gnu-patch-even-on-macos/index.html @@ -15,7 +15,7 @@ 【Composer】 composer-patches v2 では macOS でも GNU patch のインストールが不要になる (予定)|REPL: Rest-Eat-Program Loop - +

      @@ -126,8 +126,9 @@ ワークアラウンドとして、macOS にも GNU patch をインストールしてしまうという方法がある。例:

      -
      $ brew install gpatch -
      $ echo 'PATH="/opt/homebrew/opt/gpatch/libexec/gnubin:$PATH"' >> ~/.zshrc
      +
      $ brew install gpatch
      +$ echo 'PATH="/opt/homebrew/opt/gpatch/libexec/gnubin:$PATH"' >> ~/.zshrc
      +

      GNU patch を Homebrew などの手段でインストールし、BSD patch よりも優先されるパスに配置すれば問題が解消する。 diff --git a/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html b/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html index a5bbbcea..cef5c6e1 100644 --- a/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html +++ b/services/nuldoc/public/blog/posts/2025-05-05/make-tiny-self-hosted-c-compiler/index.html @@ -15,7 +15,7 @@ セルフホスト可能な C コンパイラを作った|REPL: Rest-Eat-Program Loop - +

      @@ -359,9 +359,10 @@ compilerbook では整数一つのパース・コード生成から始めるが、今回は以下のようなソースをパースしてコード生成するところからスタートすることにした。

      -
      int main() { -
      return 42; -
      }
      +
      int main() {
      +    return 42;
      +}
      +

      この時点で、struct Tokenstruct Parserstruct AstNodestruct CodeGen といった主要なデータ構造が定義され、この後もほぼ同じソース設計のまま進めている。 @@ -408,23 +409,24 @@ 一日の終わりには、次のようなプログラムのテストが通るようになった。

      -
      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; -
      }
      +
      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;
      +}
      +
      @@ -513,13 +515,14 @@ 記念すべき (?) 最後のバグはこちら。

      -
      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); -
      } -
      }
      +
               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);
      +     }
      + }
      +

      メモリアドレスから参照先の値を得る際、その型によってロードする命令の種類を変える必要があるのだが、その切替をポインタ型でおこなっていた。正しくは、そのポインタ型が指す型を元にして切り替えなければならない。 @@ -534,16 +537,17 @@ 一体どこが異なるのか。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|
      +
      $ 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_errorread_alltokenize type_new はいずれも main.c で定義された関数の名前である。このことから考えると、これは GCC が埋め込んだシンボルテーブルである可能性が高い。わずかに異なっている 6バイトは、ランダム生成された何かのように見える。 diff --git a/services/nuldoc/public/blog/posts/2025-06-14/baba-is-you/index.html b/services/nuldoc/public/blog/posts/2025-06-14/baba-is-you/index.html index b11584b7..28ed964e 100644 --- a/services/nuldoc/public/blog/posts/2025-06-14/baba-is-you/index.html +++ b/services/nuldoc/public/blog/posts/2025-06-14/baba-is-you/index.html @@ -15,7 +15,7 @@ 最高のパズルゲーム Baba Is You をやれ|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/2025-07-15/partial-surrender-to-ebooks/index.html b/services/nuldoc/public/blog/posts/2025-07-15/partial-surrender-to-ebooks/index.html index 55b9888c..305551cd 100644 --- a/services/nuldoc/public/blog/posts/2025-07-15/partial-surrender-to-ebooks/index.html +++ b/services/nuldoc/public/blog/posts/2025-07-15/partial-surrender-to-ebooks/index.html @@ -14,7 +14,7 @@ 電子書籍への部分的降伏|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html b/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html index a4a4c0aa..df401e12 100644 --- a/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html +++ b/services/nuldoc/public/blog/posts/2025-10-31/representing-single-value-with-half-open-float-interval/index.html @@ -15,7 +15,7 @@ 浮動小数点数の半開区間で単一値を表現する|REPL: Rest-Eat-Program Loop - +
      @@ -152,8 +152,9 @@ 1p のビット列での表現を見てみよう。

      -
      1 = 0011111111110000000000000000000000000000000000000000000000000000 -
      p = 0011111111110000000000000000000000000000000000000000000000000001
      +
      1 = 0011111111110000000000000000000000000000000000000000000000000000
      +p = 0011111111110000000000000000000000000000000000000000000000000001
      +

      p1 よりも一つ分だけ大きいのがわかるだろうか (ここでは binary64 の具体的な表現について言及していないのでそうなる保証はないのだが、あくまで雰囲気として)。 @@ -182,37 +183,39 @@ binary64 とそのビット表現を相互に変換できるなら、他の言語でもほとんど同じ方法で実装できるはずだ。

      -
      public static function nextUp(float $x): float -
      { -
      // NaN (Not a Number) なら NaN を返す。 -
      if (is_nan($x)) { -
      return NAN; -
      } -
      // 正の無限大なら正の無限大を返す。 -
      if (is_infinite($x) && $x > 0) { -
      return INF; -
      } -
      // 0 なら minValue() を返す (後述)。 -
      if ($x === 0.0) { -
      return self::minValue(); -
      } -
      // binary64 のビット表現を 64 bit 整数として得る。 -
      $u = self::floatToInt($x); -
      // 正なら整数に +1 して binary64 に戻す。 -
      // 負なら整数に -1 して binary64 に戻す。 -
      return $x > 0.0 ? self::intToFloat($u + 1) : self::intToFloat($u - 1); -
      }
      +
          public static function nextUp(float $x): float
      +    {
      +        // NaN (Not a Number) なら NaN を返す。
      +        if (is_nan($x)) {
      +            return NAN;
      +        }
      +        // 正の無限大なら正の無限大を返す。
      +        if (is_infinite($x) && $x > 0) {
      +            return INF;
      +        }
      +        // 0 なら minValue() を返す (後述)。
      +        if ($x === 0.0) {
      +            return self::minValue();
      +        }
      +        // binary64 のビット表現を 64 bit 整数として得る。
      +        $u = self::floatToInt($x);
      +        // 正なら整数に +1 して binary64 に戻す。
      +        // 負なら整数に -1 して binary64 に戻す。
      +        return $x > 0.0 ? self::intToFloat($u + 1) : self::intToFloat($u - 1);
      +    }
      +

      0 のときに返している minValue() は次のような値である。

      -
      public static function minValue(): float -
      { -
      // 整数の 1 を binary64 と解釈した値を返す。 -
      // binary64 で表せる最小の正の非正規化数。 -
      return self::intToFloat(1); -
      }
      +
          public static function minValue(): float
      +    {
      +        // 整数の 1 を binary64 と解釈した値を返す。
      +        // binary64 で表せる最小の正の非正規化数。
      +        return self::intToFloat(1);
      +    }
      +
      diff --git a/services/nuldoc/public/blog/posts/2025-11-09/rubiks-cube-blindfolded-first-success/index.html b/services/nuldoc/public/blog/posts/2025-11-09/rubiks-cube-blindfolded-first-success/index.html index f835dd5e..e6bfec4e 100644 --- a/services/nuldoc/public/blog/posts/2025-11-09/rubiks-cube-blindfolded-first-success/index.html +++ b/services/nuldoc/public/blog/posts/2025-11-09/rubiks-cube-blindfolded-first-success/index.html @@ -15,7 +15,7 @@ ルービックキューブを目隠しで揃えることに初成功した|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html b/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html index 845f739a..870adbe1 100644 --- a/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html +++ b/services/nuldoc/public/blog/posts/2025-11-27/anybatross-writeup/index.html @@ -15,7 +15,7 @@ カヤックさん開催のコードゴルフコンテスト Anybatross に参加して優勝した|REPL: Rest-Eat-Program Loop - +
      @@ -132,7 +132,8 @@

      回答 (45 byte)

      -
      print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
      +
      print$a+=$\=y/8B/0/+y/0469ADO-R//.$/,","for<>
      +

      Hole 1 については同一言語・同一スコアの回答が複数あるので詳細は省略する。 @@ -150,10 +151,11 @@ 最終スコアを見ると 4 位タイ (107 byte) が多く、3 位以上の回答と明確にアルゴリズムの差があるのでここから解説をスタートしようと思う。

      -
      s=gets -
      ?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2} -
      m>1&&(s.gsub!b,it;$*<<it+?:+b)} -
      puts$**?,,s
      +
      s=gets
      +?A.upto(?Z){(b,),m=s.scan(/(?=(.\B.))/).tally.max_by{_2}
      +m>1&&(s.gsub!b,it;$*<<it+?:+b)}
      +puts$**?,,s
      +

      変数名などの細かい差異を除けば他の 107 byte 回答と同じだが、 String#scan に渡す正規表現にこれを採用していたのは私だけだったのではないだろうか。 /(?=(\S\S))//(?=(\w\w))/ と比べて短くはならないので意味はない。 @@ -168,9 +170,10 @@ Enumerable#max_by で最頻値を取ってきた後は、多重代入を使って必要な値を取り出している。

      -
      x = [["la"], 3] -
      (b,),m = x -
      # => b = "la", m = 3
      +
      x = [["la"], 3]
      +(b,),m = x
      +# => b = "la", m = 3
      +

      置換テーブルのデータは $* へと追加しているが、これは Ruby の特殊変数で、本来は Object::ARGV を指す。ここでは単に最初から空配列で初期化されている便利な入れ物として用いている。 @@ -185,10 +188,11 @@ 回答 A をぐっと睨むと、m>1&&(...) の括弧を削りたくなる。しかしそれには m>1&& がどうしても邪魔になる。というわけで終了条件を工夫することでなんとか m を排除できないかを考えた。それがこちら。

      -
      s=gets -
      ?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2} -
      $*<<it+?:+b if s.gsub!b,it} -
      puts$**?,,s
      +
      s=gets
      +?A.upto(?Z){(b,),=(?_+s).scan(/(?=(.\B.))/).tally.max_by{_2}
      +$*<<it+?:+b if s.gsub!b,it}
      +puts$**?,,s
      +

      s の先頭に番兵 _ を置くことで、bi-gram の出現頻度がすべて 1 になったとき、b へと代入される値が「_ + (s の先頭の文字)」になる。これを String#gsub! で置き換えようとすると、そのような文字列は s 中にないので置換が発生しない。String#gsub! は置換が起きなかったとき nil を返すので、これを使って条件分岐ができる。&& だと優先度の関係から String#gsub! の括弧が省略できないが、後置 if なら省略できる。 @@ -203,10 +207,11 @@ Kernel#gets は、入力を特殊変数 $_ へ代入する。これは Perl 由来の挙動で、Ruby にはいくつか $_ を参照するものがある。これを使って変数 s を置き換えると次のようになる。

      -
      gets -
      ?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2} -
      $*<<it+?:+b if$_.gsub!b,it} -
      puts$**?,,$_
      +
      gets
      +?A.upto(?Z){(b,),="_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}
      +$*<<it+?:+b if$_.gsub!b,it}
      +puts$**?,,$_
      +

      これで 1 byte 縮む。 @@ -218,9 +223,10 @@ 回答 C を眺めると、b への代入に文字を費やしすぎている。これを String#gsub! の第一引数に直接書いてはどうか。更に、直前のマッチしたパターンを指す特殊変数 $& を使えば、変数 b を排除できる。それがこちら。

      -
      gets -
      ?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it} -
      puts$**?,,$_
      +
      gets
      +?A.upto(?Z){$*<<it+?:+$&if$_.gsub!"_#$_".scan(/(?=(.\B.))/).tally.max_by{_2}[0][0],it}
      +puts$**?,,$_
      +

      これにより 2 bytes も一気に縮まった。 @@ -232,9 +238,10 @@ 回答 D を提出したことで tompng 氏のスコアを越え、氏のコードを閲覧できるようになった。そこから少し変更したものが、mame 氏と (変数名などの些事を除いて) 同じ以下のコードである。

      -
      s=gets -
      ?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)} -
      puts$**?,,s
      +
      s=gets
      +?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;s.gsub!b,it)}
      +puts$**?,,s
      +

      ここまでとは大きく異なる戦略で終了条件を判定している。使われているのはパターンマッチで、in がマッチの有無を true / false で返すことを利用している。or を用いて、最頻値の出現回数が 1 でないなら置換処理を継続する。 @@ -243,10 +250,11 @@ パターンマッチの利用については途中何度か検討したが、1 でないときに処理を実行するという方針で実装しようとしてしまい、上手く短縮できなかった。

      -
      # これは 106 byte -
      s=gets -
      ?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)} -
      puts$**?,,s
      +
      # これは 106 byte
      +s=gets
      +?A.upto(?Z){s.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],2..and($*<<it+?:+b;s.gsub!b,it)}
      +puts$**?,,s
      +
      @@ -266,10 +274,11 @@ ruby-p を付けると、以下のようなコードを書いたかのように動作する。

      -
      while gets -
      ... # 記載したコードの処理 -
      puts $_ -
      end
      +
      while gets
      +  ... # 記載したコードの処理
      +  puts $_
      +end
      +

      また、Kernel#gsub という $_ = $_.gsub(...) と同様の処理をおこなうメソッドが生えてくる。今回は String#gsub! も使うので、shebang の分を回収できれば短縮になりそうだ。 @@ -278,18 +287,20 @@ というわけで、実はこれまでも shebang での短縮は何度か試していた。しかし、いずれも 1 byte 増えたり変化しなかったりで成果を上げられずにいた。回答 E についても同様に、以下のようなコードを作っていた。

      -
      #!ruby -p -
      ?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)} -
      puts$**?,
      +
      #!ruby -p
      +?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or($*<<it+?:+b;gsub b,it)}
      +puts$**?,
      +

      しかしこれは 103 byte で縮められない。にっくきは gsubb の間のスペースである。せっかく s.gsub!gsub にしたのに、後ろが記号でなくなったことでスペースが生じている。といって、括弧を付けるのも上手くはいかない。

      -
      # これも同じく 103 byte -
      #!ruby -p -
      ?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)} -
      puts$**?,
      +
      # これも同じく 103 byte
      +#!ruby -p
      +?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b&&gsub(b,it)}
      +puts$**?,
      +

      外側の括弧を移動させてくれば gsubb の間のスペースを消せるが、;&& にせねばならず失敗する。この問題を解決したのが最終回答の 102 byte コードである。 @@ -298,9 +309,10 @@

      最終回答 (102 byte)

      -
      #!ruby -p -
      ?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)} -
      puts$**?,
      +
      #!ruby -p
      +?A.upto(?Z){$_.scan(/(?=(.\B.))/).tally.max_by{_2}in[b],1or$*<<it+?:+b%gsub(b,it)}
      +puts$**?,
      +

      String#% は文字列のフォーマット処理をおこなう演算子だが、ここでは特にフォーマット目的で呼んでいるわけではない。ここで重要なのは、この演算子が特に副作用を持たず、どんな型でも右辺に取れることである。b の中身にフォーマット指定子はない (% などの記号が入力されないことが問題文から分かる) ので、誤って動作を壊してしまうおそれもない。 diff --git a/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html b/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html index fcd39446..34f13c57 100644 --- a/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html +++ b/services/nuldoc/public/blog/posts/2025-12-06/archive-dynamic-site-with-wget/index.html @@ -14,7 +14,7 @@ wget を使って動的サイトを静的サイトにアーカイブする|REPL: Rest-Eat-Program Loop - +

      @@ -101,41 +101,42 @@ 今回使用したスクリプトはこちら: https://github.com/nsfisis/phperkaigi-2024-albatross-archive/blob/cc837f6d2109555e2392016e8f6820fb5fd46dd6/archive.sh

      -
      # 指定した URL からスタートしてリンクを辿りながら全ファイルをファイルに書き出す -
      # -
      # --mirror リンクを再帰的に辿ってダウンロードする -
      # --page-requisites CSS や画像等も含めて HTML から参照されている全ファイルをダウンロードする -
      # --convert-links リンクを相対リンクへ変換する -
      # --adjust-extension URL に拡張子が無くてもいい感じに推測する -
      # --no-parent 親ディレクトリは見に行かない -
      # --no-wait=1 リクエスト間で 1 秒待機する -
      # -P ./archive/ 指定したディレクトリに保存する -
      wget \ -
      --mirror \ -
      --page-requisites \ -
      --convert-links \ -
      --adjust-extension \ -
      --no-parent \ -
      --wait=1 \ -
      -P ./archive/ \ -
      https://t.nil.ninja/phperkaigi/2024/golf/ -
      -
      # ディレクトリ構造を調整する -
      mv ./archive/t.nil.ninja/phperkaigi/2024/golf/* ./archive -
      rmdir ./archive/t.nil.ninja/phperkaigi/2024/golf/ -
      rmdir ./archive/t.nil.ninja/phperkaigi/2024/ -
      rmdir ./archive/t.nil.ninja/phperkaigi/ -
      rmdir ./archive/t.nil.ninja/ -
      -
      mkdir -p ./archive/api/quizzes/{1,2,3} -
      -
      # 動的な API エンドポイントを叩いて結果を JSON ファイルとして保存する -
      wget -O ./archive/api/quizzes/1/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/1/chart -
      wget -O ./archive/api/quizzes/2/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/2/chart -
      wget -O ./archive/api/quizzes/3/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/3/chart -
      -
      # 上記 API を叩いている箇所を、落としてきた静的ファイルを参照するように変更する -
      sed -i -e 's#/chart`#/chart.json`#' ./archive/assets/chart.js
      +
      # 指定した URL からスタートしてリンクを辿りながら全ファイルをファイルに書き出す
      +#
      +# --mirror  リンクを再帰的に辿ってダウンロードする
      +# --page-requisites  CSS や画像等も含めて HTML から参照されている全ファイルをダウンロードする
      +# --convert-links  リンクを相対リンクへ変換する
      +# --adjust-extension  URL に拡張子が無くてもいい感じに推測する
      +# --no-parent  親ディレクトリは見に行かない
      +# --no-wait=1  リクエスト間で 1 秒待機する
      +# -P ./archive/  指定したディレクトリに保存する
      +wget \
      +    --mirror \
      +    --page-requisites \
      +    --convert-links \
      +    --adjust-extension \
      +    --no-parent \
      +    --wait=1 \
      +    -P ./archive/ \
      +    https://t.nil.ninja/phperkaigi/2024/golf/
      +
      +# ディレクトリ構造を調整する
      +mv ./archive/t.nil.ninja/phperkaigi/2024/golf/* ./archive
      +rmdir ./archive/t.nil.ninja/phperkaigi/2024/golf/
      +rmdir ./archive/t.nil.ninja/phperkaigi/2024/
      +rmdir ./archive/t.nil.ninja/phperkaigi/
      +rmdir ./archive/t.nil.ninja/
      +
      +mkdir -p ./archive/api/quizzes/{1,2,3}
      +
      +# 動的な API エンドポイントを叩いて結果を JSON ファイルとして保存する
      +wget -O ./archive/api/quizzes/1/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/1/chart
      +wget -O ./archive/api/quizzes/2/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/2/chart
      +wget -O ./archive/api/quizzes/3/chart.json https://t.nil.ninja/phperkaigi/2024/golf/api/quizzes/3/chart
      +
      +# 上記 API を叩いている箇所を、落としてきた静的ファイルを参照するように変更する
      +sed -i -e 's#/chart`#/chart.json`#' ./archive/assets/chart.js
      +

      このように wget に適切なオプションを渡すことで、指定したページから遷移可能なページを再帰的に辿っていき、サイト内の全ページをファイルへ落とすことができる。今回のサイトにはページ遷移では辿り着けないページがあったが (管理画面など)、運用が停止している今そういったページはアーカイブしなくてもよい。 @@ -144,16 +145,17 @@ あとはこのファイルを適当にサーブしてやればよい。

      -
      server { -
      listen 80 default; -
      listen [::]:80; -
      -
      charset UTF-8; -
      -
      location /phperkaigi/2024/golf/ { -
      alias /archive/; -
      } -
      }
      +
      server {
      +    listen 80 default;
      +    listen [::]:80;
      +
      +    charset UTF-8;
      +
      +    location /phperkaigi/2024/golf/ {
      +        alias /archive/;
      +    }
      +}
      +
      diff --git a/services/nuldoc/public/blog/posts/2025-12-31/2025-reflections/index.html b/services/nuldoc/public/blog/posts/2025-12-31/2025-reflections/index.html index ba09e390..7348f9f0 100644 --- a/services/nuldoc/public/blog/posts/2025-12-31/2025-reflections/index.html +++ b/services/nuldoc/public/blog/posts/2025-12-31/2025-reflections/index.html @@ -14,7 +14,7 @@ 2025 年の振り返り|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2026-01-16/development-environment-2026/index.html b/services/nuldoc/public/blog/posts/2026-01-16/development-environment-2026/index.html index 23b7fab1..1787c678 100644 --- a/services/nuldoc/public/blog/posts/2026-01-16/development-environment-2026/index.html +++ b/services/nuldoc/public/blog/posts/2026-01-16/development-environment-2026/index.html @@ -15,7 +15,7 @@ 開発環境現状確認 2026|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2026-02-01/rewrite-this-site-generator-2026/index.html b/services/nuldoc/public/blog/posts/2026-02-01/rewrite-this-site-generator-2026/index.html index 332dd041..29b1e394 100644 --- a/services/nuldoc/public/blog/posts/2026-02-01/rewrite-this-site-generator-2026/index.html +++ b/services/nuldoc/public/blog/posts/2026-02-01/rewrite-this-site-generator-2026/index.html @@ -15,7 +15,7 @@ このサイトの静的サイトジェネレータを書き直した (2026)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/2026-04-20/i-can-eat-glass-it-doesnt-hurt-me/index.html b/services/nuldoc/public/blog/posts/2026-04-20/i-can-eat-glass-it-doesnt-hurt-me/index.html index 2e8d3cfc..acd48062 100644 --- a/services/nuldoc/public/blog/posts/2026-04-20/i-can-eat-glass-it-doesnt-hurt-me/index.html +++ b/services/nuldoc/public/blog/posts/2026-04-20/i-can-eat-glass-it-doesnt-hurt-me/index.html @@ -14,7 +14,7 @@ 私はガラスを食べられます。それは私を傷つけません|REPL: Rest-Eat-Program Loop - +
      @@ -219,10 +219,11 @@

      -
      * GLASS -
      * Kermit project's "I Can Eat Glass" list, also available in pango-view/ -
      * http://www.columbia.edu/kermit/utf8.html#glass -
      * Fetched on 2008-08-19, updates on 2020-09-08
      +
       * GLASS
      + * 	Kermit project's "I Can Eat Glass" list, also available in pango-view/
      + * 	http://www.columbia.edu/kermit/utf8.html#glass
      + * 	Fetched on 2008-08-19, updates on 2020-09-08
      +

      @@ -230,11 +231,12 @@

      -
      LANGUAGE( -
      ja /* Japanese */, -
      KERMIT, -
      "いろはにほへと ちりぬるを 色は匂へど 散りぬるを" -
      )
      +
      LANGUAGE(
      +        ja /* Japanese */,
      +        KERMIT,
      +        "いろはにほへと ちりぬるを 色は匂へど 散りぬるを"
      +        )
      +

      diff --git a/services/nuldoc/public/blog/posts/2026-05-31/regional-rubykaigi-sekigahara01/index.html b/services/nuldoc/public/blog/posts/2026-05-31/regional-rubykaigi-sekigahara01/index.html index 5097e192..e9730df9 100644 --- a/services/nuldoc/public/blog/posts/2026-05-31/regional-rubykaigi-sekigahara01/index.html +++ b/services/nuldoc/public/blog/posts/2026-05-31/regional-rubykaigi-sekigahara01/index.html @@ -15,7 +15,7 @@ 関ケ原Ruby会議01 参戦記|REPL: Rest-Eat-Program Loop - +

      diff --git a/services/nuldoc/public/blog/posts/3/index.html b/services/nuldoc/public/blog/posts/3/index.html index b36920ea..15db744f 100644 --- a/services/nuldoc/public/blog/posts/3/index.html +++ b/services/nuldoc/public/blog/posts/3/index.html @@ -15,7 +15,7 @@ 投稿一覧 (3ページ目)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/4/index.html b/services/nuldoc/public/blog/posts/4/index.html index 66af42d2..9e0e72ec 100644 --- a/services/nuldoc/public/blog/posts/4/index.html +++ b/services/nuldoc/public/blog/posts/4/index.html @@ -15,7 +15,7 @@ 投稿一覧 (4ページ目)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/5/index.html b/services/nuldoc/public/blog/posts/5/index.html index 9dfa3bdc..a1c1f92d 100644 --- a/services/nuldoc/public/blog/posts/5/index.html +++ b/services/nuldoc/public/blog/posts/5/index.html @@ -15,7 +15,7 @@ 投稿一覧 (5ページ目)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/6/index.html b/services/nuldoc/public/blog/posts/6/index.html index cdde5fd6..bfd03dd0 100644 --- a/services/nuldoc/public/blog/posts/6/index.html +++ b/services/nuldoc/public/blog/posts/6/index.html @@ -15,7 +15,7 @@ 投稿一覧 (6ページ目)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/7/index.html b/services/nuldoc/public/blog/posts/7/index.html index 9435cf6d..87028914 100644 --- a/services/nuldoc/public/blog/posts/7/index.html +++ b/services/nuldoc/public/blog/posts/7/index.html @@ -15,7 +15,7 @@ 投稿一覧 (7ページ目)|REPL: Rest-Eat-Program Loop - +
      diff --git a/services/nuldoc/public/blog/posts/index.html b/services/nuldoc/public/blog/posts/index.html index 1ef70446..555a705a 100644 --- a/services/nuldoc/public/blog/posts/index.html +++ b/services/nuldoc/public/blog/posts/index.html @@ -15,7 +15,7 @@ 投稿一覧 (1ページ目)|REPL: Rest-Eat-Program Loop - +
      -- cgit v1.3.1