INTERCAL を調べてみた

INTERCAL は 1972 年に登場した難解言語 (esoteric language) の始祖とされる言語です。brainfuck や befunge のような難解言語に興味のある人なら、一度は聞いたことがあると思います。
しかし、INTERCAL について、「元祖難解言語であること」と Hello, world! 以上の詳しいことをわかりやすく説明する資料は見当たりません。マニュアルはなぜかやたら読みにくいので敬遠していたんですが、一念発起して、多少読み書きできる程度まで勉強してみました。元祖の名に恥じない、なかなか頭の悪い言語でした。

変数と定数

16 ビット整数、32 ビット整数、16 ビット整数の配列、32 ビット整数の配列、の 4 種類の型がある。それぞれ . (spot) 、: (twospot) 、, (tail) 、; (hybrid) の記号が与えられている (括弧は呼び方) 。
変数名は「型を表す記号」+「1 から 65535 の数字」と書く。例えば、.1.2 は 16 ビット変数の名前。
定数は最初に # をつけて 10 進表現で書く。#0 とか #1 とか。ただし定数としてかけるのは 65535 までで、65536 以上の数字は演算によって作らないといけない。

演算子

INTERLEAVE (MINGLE とも) 、SELECT 、AND 、OR 、XOR の 5 つ。

INTERLEAVE

2 引数の中置演算子。実際に書く記号は ¢ 。ただし環境や処理系によってコード上での表現が変わる。元祖である INTERCAL-72 では EBCDIC での c + backspace + / の 3 バイト *1 。環境によっては $ で代用している。Latin-1 (ISO-8859-1) や UTF-8 での ¢ を認識する処理系もあるとか。
INTERLEAVE は、2 つの引数のビット列表現を交互に並べた値を返す。1 つめの引数のビット列表現が ABCD... で、2 つめが abcd... なら、AaBbCcDd... という感じ。例えば #12¢#5 なら、0b1100 と 0b101 で 0b10110001 、つまり 177 が得られる。
INTERLEAVE の使用例としては、定数で書けない 65536 のような数字を、#0¢#256 と書くとか。

SELECT

2 引数の中置演算子。記号は ~
SELECT は 1 つめの引数のビット列表現から、2 つめの引数のビット列表現の対応する桁が 1 になっているところだけ抽出して、上位ビットを 0 でパディングした値を返す。1 つめの引数のビット列表現が ABCDEFGH で、2 つめが例えば 01010101 なら、 0000BDFH を返す。例えば #51~#21 なら、0b110011 と 0b10101 で 0b101 、つまり 5 が得られる。

AND 、OR 、XOR

他の言語とちがって、1 引数の演算子。記号はそれぞれ &V 。例によって は環境依存で、INTERCAL-72 では V + backspace + - の 3 バイト。?\ で代用する処理系もあるらしい *2
1 引数の演算子だが、中置演算子 *3 。どういうことかというと、式を表す文字列の 1 文字目と 2 文字目の間に置く。例えば #1 を AND 演算する場合は #&1 となる。括弧式 (後述) の場合も同様に、'#1~#1' の AND 演算は '&#1~#1' となる。
これらの演算子は、引数をビット列表現したときに、隣り合うビットを演算した結果を返す。引数のビット列表現が ABCDEFGH の場合、(H op A) (A op B) (B op C) ... (G op H) を返す。例えば、#26 は 0b11010 なので、#&26 は 0b1000 、#V26 は 0b11111 、#∀26 は 0b10111 。

普通の 2 引数の AND とかをやりたいときは、

  • 2 引数を INTERLEAVE する
  • その結果を AND する
  • その結果を #0¢#65535 (= 0b010101...) で SELECT する

とやるみたい。かっこいい。

演算子の優先順位

演算子に優先順位はないため、あいまいさが生じる場合には必ず括弧でくくる必要がある。使用できる括弧の記号は ' (spark) と " (rabbit-ears)。
慣習的に '" を交互に使う。'...' の内側は "...""..." の内側は '...' という感じ。ただし、片方だけでもあいまいなく書くことができるので、かっこよく書きたい人はそうしてもいい *4
' の直後に . が来る場合は、2 つの文字を合体して ! と書くことができる。かっこいい!

配列添字

演算子ではないらしいけどついでに。配列から n 番目の値を取り出すには SUB を使う。配列 ,1 から 1 番目の値を取り出すには、,1 SUB #1 などと書く。多次元配列もサポートしていて、(1, 1) 番目を取り出すには ,1 SUB #1 #1 とか。

1 つの文は「行番号」「文識別子」「本体」からなる。

行番号

分岐先とかの指示で使うラベル。括弧付きで書く。(10) とか。昇順である必要はない。また、必要でなければ書かなくても良い。

文識別子

DO または PLEASE または PLEASE DO 。どれを書いてもいいが、PLEASE がついてる方が丁寧。処理系によってはある程度 PLEASE を使わないと「プログラマーが無礼だ」といってエラーを吐く。PLEASE を使いすぎると「プログラマー慇懃無礼だ」といってエラーを吐く。DO 3 つにつき PLEASE 1 つくらいがいいらしい。

文識別子の直後に NOT または N'T を付けると、その行は実行されなくなる。これを利用してコメントを書ける。後述。

ちなみに INTERCAL では空白は完全に無視される。キーワードと数値リテラルの間以外ならどこにどれだけ入れてもいいし、全く入れなくても良い。なので、DON'T などと書いてもいい *5

代入文

文識別子の後に特にキーワードを書かないと代入文となる。代入文は DO .1 <- #1~#1 のように書く。これは #1~#1 を計算して、その結果を変数 .1 に代入している。DO ,1 SUB #1 <- #10 などとすると配列の 1 番目に 10 を代入する。
配列自体への代入は特別で、配列の大きさを代入した値にする。つまり DO ,1 <- #10,1 を長さ 10 の配列とする。DO ,1 <- #10 BY #10 とすると 10x10 の多次元配列とする。

制御構造

DO (行番号) NEXT で指定された行へ分岐する。その際、NEXT スタックにこの NEXT のあった次の行の番地 (要するにリターンアドレス) を push する。
DO RESUME 数 は NEXT スタックから指定された数だけ pop して、最後に pop した番地に分岐する。1 を指定すれば普通のリターン。DO RESUME #10 などとすると 10 個上のスタックフレームまで飛ぶ例外みたいな感じ。0 やスタックの長さより大きい数を指定したらエラー。
DO FORGET 数 は NEXT スタックから指定された数だけ pop するだけ。分岐はしない。0 を指定したら何もしない。スタックの長さより大きい数を指定したらスタックを空にする。
RESUME の性質を利用しやすいので、INTERCAL の真偽値には 1 と 2 を使う場合が多いらしい。以下は条件分岐の例。.1 が 1 なら then 節を、2 なら else 節を実行する。

    DO (1) NEXT
    (else 節)
    DO (3) NEXT
(1) DO (2) NEXT
    DO FORGET #1
    (then 節)
    DO (3) NEXT
(2) DO RESUME .1
(3) DO FORGET #1
変数スタック

すべての変数は、それぞれ独自の隠しスタックを持っている。DO STASH .1 とすると、.1 の現在の値を隠しスタックに push する。DO RETRIEVE .1 すると .1 の隠しスタックを pop して、その値を .1 に代入する (代入前の値は捨てられる) 。
この機能は普通に便利かもと思ってしまった。CLC-INTERCAL だと Cannot STASH numbers とか言われてなんか動かない。

変数の書き込み可否

DO IGNORE .1 とすると、変数 .1 が read-only になり、.1 への代入が無視されるようになる。DO REMEMBER .1 とすると元に戻る。
何に使うのかよくわかってない。システムライブラリ用とかなんとか。

行の有効化・無効化

DO ABSTAIN FROM 行番号 とすると、指定された行が無効化される (その行に NOT を付けたのと同じ効果) 。
DO REINSTATE 行番号 とすると、指定された行が有効化される。
また、行番号の代わりに動名詞 (NEXT なら NEXTING 、RESUME なら RESUMING とか) を指定することで、そのコマンドをすべて無効化したり有効化したりもできる。例えば DO ABSTAIN FROM ABSTAINING + REINSTATING などと書くと、ABSTAIN と REINSTATE を実行しないようになる (ただし REINSTATE を無効化すると元に戻せないので注意) 。

READ OUT 、WRITE IN

入出力。
READ OUT は出力。DO READ OUT 整数 は、その整数を標準出力に出力する。ただし出力形式はローマ数字 (I とか II とか V とか) 。
WRITE IN は入力。DO WRITE IN 変数 は、標準入力から整数を読み込んで変数に代入する。ただし入力形式は英単語の列 (SIX FIVE FIVE THREE FIVE とか) 。

C-INTERCAL の独自拡張で、1 バイトずつ入出することもできるが、無駄にややこしい。DO READ OUT 配列 で、配列の整数列を出力する。ただし、その数字のキャラクタコードが出力されるわけではなく、(直前に出力した数字 - 出力する数字) mod 256 の値のビット列表現を反転 *6 した値、を出力する。INTERCAL での Hello, world! を見ながら考えるといいと思う。入力は (直前に入力した数字 - 入力した数字) mod 256 で、ビット列の反転はしないらしい (けど試してない) 。

GIVE UP

実行を終了する。これがないとエラーになる。

syntax error

上記のどれでもない行は syntax error になる。INTERCAL では syntax error な行を実行すると、その行をエラーメッセージとしてエラーを吐く。

DOUBLE OR SINGLE PRECISION ARITHMETIC OVERFLOW

は INTERCAL のシステムライブラリの中で使われている、エラーを報告する文。DO で始まっていることに注意。
逆に言うと、INTERCAL では syntax error な行でも、その行が実行されるまでエラーを吐かない。つまり、DO NOT で始まる syntax error な行は無視される。このため、これをコメントとして使う。
ただし REINSTATE でコメントを実行可能にされてしまう可能性があるので、PLEASE NOTE THAT THIS IS A COMMENT のように PLEASE NOTE で始めることが多い。PLEASE NOTE の間で切れていることに注意。E で始まるコマンドがないため、この行は実行可能にされてもエラーを報告するだけで、怪しく実行が続くことがない。

派生

バリエーションや方言がいっぱいあって、COME FROM を追加したとか、Threaded INTERCAL (マルチスレッド) とか、Backtracking INTERCAL (文を実行する確率が書けるらしい) とか、Quantum INTERCAL (量子計算) とか。

感想

brainfuck のようなシンプルさはないですが、文法も制御構造もなかなかすてきな言語だと思いました。! みたいなのは好きですね。
「指定した数だけリターン」とか変数スタックとかは、普通に便利そうに感じてしまいます。「指定した数だけリターン」ができれば my_return をメソッドとして定義できるんだよなあ。Ruby にも欲しい。

*1:タイプライタやプリンタのようなハードウェアで印字すると、c を印字した上に / を印字するので¢みたいな字になるということだと思う。INTERCAL 独自のネタなのか、当時のハードウェアでは一般的な手法だったのか、よくわからない。

*2:? である理由は、「XOR を初めて見た人の標準的な反応を適切に表現する」から。

*3:処理系によっては前置もサポートしているらしい。

*4:ただし多次元配列の添字の中だけはあいまいになることがあるので、その場合は両方使う必要がある。

*5:今にしてみると気持ち悪い話だけど、ぼくの知る限り N88-BASIC もそんな感じだったので、昔なら普通のことだったんだろう。FORTRAN は今でもそうだとか?

*6:0 <-> 1 ではなく、LSB を MSB に、MSB を LSB にする感じ。