hacker.orgの「Super Small Hello World」チャレンジの解説記事です。うまくいった解法、惜しくも届かなかった解法、そして最終的に29バイトに到達したトリックを紹介します。
hacker.org には、SuperHack と呼ばれる少し風変わりな難解プログラミング言語(Esolang)があります。課題自体は一見すると実に単純で、「Hello, World! と出力するSuperHackプログラムを書くこと」です。
しかし、本当の勝負はスコアにあります。スコアはコード全体を囲む最小の長方形(バウンディングボックス)の面積で決まり、小さければ小さいほど高評価となります。また、プログラムは !(停止)命令を実行して正常に終了しなければなりません。テキストを出力した後にVM例外でクラッシュしたり、余計なバイトを出力したりした場合は失格となります。
打ち破るべきターゲットの数字は 29 でした。
SuperHackは、Befungeの思想に近い2次元スタックマシンです。
% の位置)からスタートし、右方向へ進みます。| 命令 | 効果 |
|---|---|
0-9 | 数字を1つプッシュする |
+ - d | 加算、減算、切り捨て除算(ゼロによる d は 0 になる) |
x | スタックの先頭(トップ)を複製する |
n ^ | スタックの先頭から n 個下にある要素のコピーをプッシュする(1^ は2番目の要素をコピーする) |
, | 入力を読み込む。入力がない場合は 0 をプッシュする |
g | x をポップし、y をポップし、座標 (x, y) にある文字コードをプッシュする。グリッド外の場合は 0 をプッシュする |
P | ポップして文字として出力する(下位7ビットのみ) |
s | 次のセルをスキップする |
? | ポップし、その値が 0 であれば次のセルをスキップする |
@ | 現在の位置(および進行方向)をコールスタックにプッシュする |
$ | 位置をポップしてそこへジャンプし、さらに2セル先から実行を再開する |
\ | ミラー(鏡):「右への移動」を「下への移動」に変える |
! | 停止(halt) |
| その他すべて | 何もしない(no-op) |
ここから導き出される2つの性質が、以降のすべての設計を方向づけます。
g 命令はプログラム自身のコードを読み取るため、Hello, World! という文字列をそのままグリッド内に配置し、1文字ずつ読み出すことができます。その文字の大半(H e l o W r)は実行されても何もしない no-op です。ただし3つだけ例外があり、, は 0 をプッシュし、d は除算を行い、! はプログラムを停止させます。@ と $ でループを作り、ループ回数は @ の配置によって決まる。
$ は戻り先の @ の2セル後から実行を再開します。そこに次の @ があれば再びそれ自身がプッシュされるため、数個の @ を並べるだけでループ本体を何度も繰り返し実行できます。@@@@@ はフィボナッチ数のループ回数を生み出します。5つ並べると、ループ本体はちょうど 13 回実行されます。@ は2の累乗のループ回数を生み出します。4つ並べると 16 回の実行が可能になります。13文字、そして13回のループ。この一致を覚えておいてください。
面積は「幅 × 高さ」であり、29は素数です。したがって、29セルのプログラムは必然的に「1行」または「1列」のどちらかになります。
Hello, World!%0@s!@@@@@ 01^gP1+$
最初の実用的な解法は、先頭に文字列データ(ペイロード)を置き、その直後から実行を開始するものです。
| 部分 | 役割 |
|---|---|
Hello, World! | データ。PCは % からスタートするため一度も実行されない |
% | エントリーポイント(開始位置) |
0 | インデックス i = 0 |
@ s ! | 停止ガジェット(halt gadget):詳細は後述 |
@@@@@ | 5つの隣接するトランポリン。これにより本体が13回実行される |
| パディング(空白、詳細は後述) |
0 1^ g | 行番号 0 をプッシュし、i をコピーして、列 i の文字を取得する |
P | 取得した文字を出力する |
1+ | i += 1 |
$ | ループで戻る |
停止ガジェットの仕組み: 最初の @ が自身の位置をコールスタックの一番下に積み、直後の s が ! を飛び越えます。その後、フィボナッチ連鎖によってループが13回実行されます。最後にコールスタックがすべて巻き戻されて最初の @ に到達すると、$ はその2セル後、つまりちょうど ! の位置から実行を再開します。これにより、文字列の終端判定を一切行うことなく、プログラムはきれいに停止します。
パディングの理由: @ が隣接している場合、最後の @ から戻るときに2セル先から再開されるため、連鎖の直後にある最初の1セルがスキップされます。そのため、そのセルには無害な文字(ここではスペース)を置いておく必要があります。
結果:Hello, World! を出力し、正常に停止。面積は 32 です。正しく動作しますが、まだ無駄があります。データ部分がただのお荷物になっており、%、パディングの空白、そして 0 で合計3セルを消費しているからです。
1H@e@l@l@o ,1 ^WgoPr2lsds!+$
次のアイデアは、独立したデータ領域にセルを費やすのをやめ、**コードの隙間に文字列を編み込む(インターリーブする)**ことです。Hello, World! の各文字を奇数列(1, 3, …, 25)に配置し、その間の偶数列をコードで埋めます。
1 でインデックスを列1(H の位置)から開始します。@(列 2, 4, 6, 8)により、最大16回のループが可能になります。,(列11)が、g 命令で行番号として必要とする 0 をプッシュしてくれます。2+ でインデックスを次の奇数列へと進めます。s を使ってデータ内の d と ! を飛び越え、コードとして実行されないようにします。問題は、16回のループが13回より多いことです。文字列を最後まで出力した後もループが止まらず、末尾を超えて文字を取得し続け、$ と2つの NUL バイトを出力してしまいます。そして最後に $ が空のコールスタックを参照し、VMがクラッシュします。
output: "Hello, World!$\0\0"
message: call stack underflow
サイズは小さいものの、有効な解答ではありません。しかし、この試行から重要なアイデアが生まれました。データ内の文字のうち、たまたま有用な命令でもある文字(,)に、「データ」と「コード」の一人二役をこなさせるという手法です。
\
2
H
@
e
@
l
@
l
@
o
x
,
g
P
W
2
o
+
r
$
l
d
!
(1行につき1文字です。空行に見える部分はデータとしての半角スペース1文字です。)
プログラムを90度回転させて縦向きにすると、思わぬメリットが生まれます。1列のプログラムでは、g 命令が必要とする x 座標が 0、行(y座標)が i になります。データ内の , がその 0 をタダで供給してくれるため、文字取得の処理が x , g(i を複製し、0 をプッシュし、取得する)だけに縮まります。追加コストは、一番上でPCを下向きに変えるための \ 1文字だけです。
ただし、これにも解法2と同じ欠陥があります。ループが13回を超えて暴走し、NUL バイトを出力した挙句、call stack underflow で終わります。面積27はこの記事の中で最小のプログラムですが、ルール上はカウントされません。
解法2と解法3から得られた教訓は、**「きれいに停止するにはセル(面積)のコストがかかる」**ということです。インターリーブを使えばループ自体は安上がりに作れますが、必要なのがちょうど13回であるにもかかわらず、16回ループしてしまいます。
\
2
H
@
e
@
l
@
l
@
o
x
,
g
P
W
x
o
5
r
d
l
5
d
?
!
2
+
$
解法4では、解法3の縦方向のインターリーブを維持しつつ、正攻法の「文字列終端テスト」を追加しました。
| 行 | セル | 役割 |
|---|---|---|
| 0 | \ | 下方向へ向きを変える |
| 1 | 2 | i = 2 |
| 3, 5, 7, 9 | @ | 4つのトランポリン(最大16回ループ) |
| 11-13 | x , g | 行 i の文字を取得(, はデータと兼用) |
| 15 | P | 出力する |
| 17-24 | x 5 d 5 d | 判定値 floor(floor(i/5)/5) = floor(i/25) を計算(最後の d はデータと兼用) |
| 25 | ? | 判定値が 0 なら次のセルをスキップする… |
| 26 | ! | …スキップされる次のセルは、データ自身の ! |
| 27-28 | 2 + | i += 2 |
| 29 | $ | ループで戻る |
インデックス i は 2, 4, …, 26 と進みます。floor(i/25) は最後の1文字(i = 26)に到達するまでずっと 0 のままなので、? は最後の1回を除くすべてのパスで停止命令をスキップします。そして最後のパスでついにデータの ! が実行されます。この ! は「最後に出力される文字」であると同時に「プログラムを停止させる命令」でもあります。
データの ,、d、! のすべてがコードとしても機能するという、非常に美しい再利用です。しかし、この終了テストに5セルを費やしているため、合計面積は 30 になります。
この時点で、私のAIアシスタントは「30が理論上の最小値である」と証明しようと試みました。どのアプローチも、同じ壁に突き当たるように見えたからです。
@ を隣接させる必要がありますが、1文字おきにデータを挟むインターリーブではコードのセルが隣接しません。そのため、16回ループ+有料の終了判定という構成から抜け出せません。? を $ の直前に置くことができません。2つの間には必ずデータのセルが挟まるからです。...d! をコードとして実行するとクラッシュまたは即時停止してしまうため、データを「決して実行されない場所」に隔離しなければならず、% や大きな開始インデックスのコストがかかってしまいます。より小さな面積に対する網羅的な機械探索でも何も見つからず、解法4のレイアウト周辺で面積29を探索しても何も見つかりませんでした。「30が下限である」という説は、もはや定理のように見え始めていました。
しかし、そうではなかったのです。
9@s!@@@@@Hello, Worlä¡1^gP1+$
突破口は、ジャッジシステムのソースコードを読み直したことから生まれました。重要なのは次の2行です。
case 'P':
$output .= chr($this->Pop() & 0x7F); break;
...
default:
break; // every unknown character is a no-op
P 命令は下位7ビットのみを出力します。つまり、最上位ビットを立てたバイト c + 128 は、出力時には c とまったく同じ文字として印字されます。つまり、データの各文字には2通りのエンコーディングが存在するということです。通常のASCII文字は実行されると何かをしてしまうことがありますが、c | 0x80(最上位ビットを立てた文字)は確実に無害(不活性)になります。これこそが「データの公式」でした。危険な文字を c + 128 として格納し、P 命令に最上位ビットをタダで削ぎ落としてもらえばよいのです。
データの中で危険な文字は2つだけです。d は ä(0xE4)になり、! は ¡(0xA1)になります。この2文字を置き換えることで、データ全体をそのままコードとして通過(実行)できるようになります。これで、先ほどの3つ目の壁が取り払われました。
さあ、解法1をベースにして、データをループの内側へと移動させましょう。
| 列 | セル | 役割 |
|---|---|---|
| 0 | 9 | i = 9(H がある列番号) |
| 1-3 | @ s ! | 解法1と同じ停止ガジェット |
| 4-8 | @@@@@ | 5つの隣接するトランポリン(本体をちょうど13回実行する) |
| 9-21 | Hello, Worlä¡ | データ。毎回のループでそのまま上を通過(実行)する |
| 22-27 | 1 ^ g P 1 + | i をコピーし、文字を取得し、出力し、インクリメントする |
| 28 | $ | ループで戻る |
1回分のパスの動きを追ってみましょう。
@ の連鎖のどこかに着地し、データの上を走り抜けます。H e l l o、スペース、W o r l は何もしません。データ内の , が、g 命令で行番号として必要とする 0 をプッシュします。 ä と ¡ も何もしません。1^ がその 0 の上に i をコピーし、g が列 i の文字を取得します。P がそれを出力します。最後の2文字については、0xE4 & 0x7F = 'd' および 0xA1 & 0x7F = '!' として出力されます。1+ が i を進め、$ がトランポリンの連鎖へと戻ります。解法1と比較すると、3つのセルが消滅しています。
%:データを実行から守る必要がなくなりました。H(no-op)になったため、専用の空白が不要になりました。1^g の前の 0:データ内の , が毎回のパスで 0 を供給してくれます。32 − 3 = 29。13回目のパスが終わると、コールスタックは停止ガジェットまで巻き戻り、プログラムは列3にある ! でピタリと停止します。
出力は Hello, World!、VMのエラーメッセージなし、271サイクル、面積 29 です。
最初の提出では、got: 'Hello, WorlC$' という結果とともにスコア31が返ってきました。Webフォームが UTF-8 で送信していたため、特殊文字がそれぞれ2バイト(ä → C3 A4、¡ → C2 A1)に変換されてしまったのです。7ビットにマスクされると、0xC3 は C として、0xA4 は $ として出力されてしまいます。
解決策は、ブラウザに Latin-1(ISO-8859-1)で送信させることです。Latin-1 なら、これらの文字はそれぞれ1バイトになります。チャレンジのページで DevTools のコンソールを開き、次のスクリプトを実行しました。
document.querySelectorAll('form').forEach(
f => f.acceptCharset = 'ISO-8859-1');
document.querySelector('textarea').value =
"9@s!@@@@@Hello, Worl\u00e4\u00a11^gP1+$";
その後、通常の送信ボタンを押します。ジャッジは Hello, World! と出力し、King of the Hill(最高記録)のスコア29が達成されました。
| # | プログラム | 面積 | 正常終了するか? |
|---|---|---|---|
| 1 | Hello, World!%0@s!@@@@@ 01^gP1+$ | 32 | ✅ |
| 2 | 1H@e@l@l@o ,1 ^WgoPr2lsds!+$ | 28 | ❌ 余分なバイト出力とコールスタック・アンダーフロー |
| 3 | \2H@e@l@l@ox,g PW2o+r$ld! | 27 | ❌ 余分なバイト出力とコールスタック・アンダーフロー |
| 4 | \2H@e@l@l@ox,g PWxo5rdl5d?!2+$ | 30 | ✅ |
| 5 | 9@s!@@@@@Hello, Worlä¡1^gP1+$ | 29 | ✅ |
, をタダの push 0 として使い、! を停止命令として使い、最終的にはデータ全体をループ本体の一部として通過させました。& 0x7F と default: break)が、何時間もの探索で届かなかった壁を打ち破ってくれました。カウエの生き方のオフライン版を入手しましょう——🍏 健康、💰 富、❤️ 愛、🧘 意味を最適化するための実践ガイドです。
新しいエッセイ、本の要約、より良く生きるための実践ガイドを公開した際にお知らせします。スパムはありません——いつでも配信停止できます。
質問、ご意見、提案はありますか?ぜひ下であなたの考えを共有してください。コメント欄は GitHub Discussions(広告なし・スパムなし)を利用しています——下の GitHub でサインイン をクリックしてコメントやリアクションを残してください(無料の GitHub アカウントは1分未満で作成できます)。
最終更新日: 2026/10/02.
免責事項:当サイトのコンテンツは情報提供および教育のみを目的としており、医療、心理、金融、投資、税務、または法律上の助言を構成するものではありません。ここに掲載されている考え、キュレーション、意見はすべて私個人のものであり、雇用主の見解を代表するものではありません。書籍、講演、記事の要約は独立した教育的な解説であり、それぞれの著者の知的財産です。編集、フォーマット、要約の補助としてAIツールを使用する場合がありますが、正確性を期すよう努めているものの誤りが含まれる可能性があります。プライバシー:当サイトでは、任意のニュースレター購読(follow.it)、コメント機能(giscus / GitHub Discussions)、および埋め込みメディア(YouTube)に外部サービスを利用しており、これらのサービスはそれぞれのプライバシーポリシーに従って訪問者のIPアドレスやCookieを処理する場合があります。