!s⊙s ' 0⊙null — 中黒(⊙)が余積 primitive、カンマは直積
johnny-shaman/Sign のセルフホスト前段 alpha/sign/ は、Sign 自身で書かれた字句解析器・パーサ・前処理器・アセンブリ出力の骨格である ── 全 1,411 行、6 ファイル。
著者が README に記した「セルフホストで見付かった 6 つの作法」を、教材として単独で読める最小サンプル 6 本に抽出した。各 40〜60 行、単一仕様に対応、末尾の駆動式まで含めて動く。
alpha/sign/ は「Sign を動かす」JavaScript 処理系と対をなす「Sign で書かれたもの」である。字句解析・構文解析・前処理・アセンブリ出力を、Sign 自身の仕様のもとで書き切っている ── ここに現れるコードは、言語設計者本人が「まとまった量を書いて初めて効いてくる」と繰り返した作法の生きた実例である。
教材はそこから 6 項目に 1 対 1 対応 させて抽出した。順番は元の README とは変え、言語の骨格 → 完全性公理 → 短絡評価 → 表引き → 局所束縛 という学習の傾斜になるよう並べ替えている。各サンプルは末尾に alpha/javascript 処理系で走らせて値が確認できる駆動式を含む。
コード本文は元ソースからの 構造抽出:識別子名を短く保ち、文脈依存の周辺コードを削り、コメントで「なぜこの書き方なのか」を仕様書のパスとともに残した。特に、著者が実測ログとして残した 2629 → 2205 命令 のような数字は改変せずそのまま引用している。
Samples 01 + 02
一様則と、直積 vs 余積。演習:is_digit と first_digit を 01 のパターンで書く。
Samples 03 + 04
器を縮める vs 添字で歩く、短絡評価。演習:join を両形で書き、なぜ後者が要るかを実測で比較する。
Samples 05 + 06
表引きと仮引数の let。演習:operator_table.sn の一段を自分で組み、cont1_naive を表引き版に書き直す。
Sign 最短の「これだけは覚える」パターン。!s(Unit の否定)と s ' 0(先頭アクセス)だけでリストの再帰が書ける ── を体感する。if / switch / return / null は一度も出ない。
drop_space
Spec 0_design_principles.md 原理5
Lines 44
Sign には真偽値型が存在しない。述語 is_space は真のとき その値(引数の文字)を返し、偽のとき __ を返す。この設計により、| は「または」ではなく「左辺が __ なら右辺」の短絡演算子として自然に働く。
!s : __ ── リストが尽きたら Unit で終わりis_space (s ' 0) : drop_space (s ' 1~) ── 空白なら残りへs ── ここまで来たら停止(分岐マーカー ? が節を切る)\n と書くと 改行ではなく「n」という文字になる。\ は次の 1 バイトをそのまま取るだけで、エスケープシーケンスは存在しない。改行は「行末に何も無い \」で書く。
「トークン列は必ずカンマで積む」の理由を String の吸収則から自分で導けるようにする。処理系のバグではなく、`123` 123 = `123123` が成り立つ以上、避けられない仕様の帰結である。
tokens
Spec type_system.md §3.2
Lines 43
Sign は 2 つの積を持つ ── 余積(⊙、字句上は空白) と 直積(,)。文字列同士を空白で並べれば連結、カンマで並べれば構造保持のリストになる。
直積の単位元は __(零対象は終対象でもあるので A × __ ≅ A)。cons セルで積んでいく書き方では、残りが尽きた終端に __ が現れない:
[`a` , [`b` , [`c` , __]]] ` == [`a` [`b` [`c`]]] ── 末尾に __ は残らない
値が __ でも キーが残る。ラベルは型の一部だから ── 位置スロットとはここが違う。
文字列は 同一行内で閉じる必要がある(sign.pegjs の string 規則が [^`\r\n]*)。改行を含む文字列は newline 定数を連接して組み立てる。
Sign 最大の落とし穴を、同じ問題(総和)の 2 通りの書き方で対比する。「終わり」を __ で表そうとすると、完全性公理により次の呼び出しが本体に一歩も入らず崩壊する ── これを分かって書くための 2 形。
末端で自然に止まる ── 尽きた瞬間に完全性公理で呼び出しが消える。リスト書き換え・生成の再帰はこちらでよい。
再帰を続けたい/終わりを値として扱いたい場面では、位置と長さの一致で終わりを決める。emit.sn の put_str や parser.sn の end_of はこの形。
parser.sn の end_of は初期形でこれをやり、無限ループしていた。再帰を続けたい場面では __ を返してはならない ── 直後の関数呼び出しがまるごと消える。
put_str が「B」を選んだのは、途中で列を縮めると末端の out # (s ' i) が __ に飲まれて 出力ゼロで止まるから。添字を進めて i >= ||s|| で終わる。| が「見付からなければ次」になる
「または」以上のもの ── ルックアップで 「見付かればそれ、無ければ既定」を 1 行で書ける。| は左辺が __ のときだけ右辺を評価する短絡演算子。__ が式全体を崩壊させる終対象なので、「値が在ること」そのものが真理値の役割を果たす。
put_infix
Spec unit.md §3.3
Lines 60
(look_up k) | `default`(mnemonic ' k~) | `.err`(emit.sn の put_infix そのまま。例外を投げないコードの原型)a | b | c(target_info.sn の char_size_of がこの形)if/switch を関数と比較で書くのではなく、「表を 1 度引く」形に畳む練習。表を持つことは「その綴りは中置演算子である」の言明そのものであり、印は要らない ── 引ければ真、__ が返れば偽。
cont1
Spec 0_design_principles.md 原理1
Lines 60
preprocess.sn の初期形 cont1_naive は 29 本の等号比較の連なりだった。表引き版 cont1 に書き換えた結果:
target_info.sn 冒頭コメント): は分岐にしか読まれない
局所束縛は 仮引数リストの側で宣言する。関数本体のインデント行は全て match_case であり、そこに 識別子 : 値 と書いても分岐にしかならない。
continues
Spec function_guide.md · unit.md §1.5
Lines 59
数式 1 つの中に「同じ問い」を何度も書くと、段の深さに対して計算量が 指数的に効く。parser.sn の作者本人が実測ログを残している:
b から取っている)target_info.sn §lit_family のコメント:既定引数のブロックで let にした digit_run を本体から呼ぶと、String の型が呼ばれた先へ伝わらず、s ' i が「まだ出せない式です」になる(実測)。このときは同じ式を 2 回書くのが唯一の書き方(unit.md §1.5「式を 2 回書く方が安い」)。
各サンプル末尾の駆動式が最後の値として返る。compile() で AST に落として、interpreter.js で 1 ノードずつ評価する形。
Sign 本体は node があれば動く。パーサはピュア JavaScript で、依存は無い。
alpha/sign/README.md の「書いてみて分かった作法」から、教材コードを自分で書くときに引っかかる項目を 4 つ抜粋した。
` を書けないコメントは行頭の ` で始まり、同じ行に閉じる ` が無い場合にコメントとなる。中に ` を入れると、そこで文字列として閉じてしまい後続がすべて壊れる。
文字列は同一行内で閉じる必要がある。改行を含む文字列は newline 定数を連接して組み立てる。
インデントは TAB でのみ書く(利用者の決定 2026-09-15)。エディタで空白展開すると壊れる。
制御文字リテラルは行末で不可視になるため、\ 直後に置くと整形処理で落ちる危険がある。改行文字(末尾に何も無い \)はこの問題を受けない。