koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

WhitespaceをC言語ソースに変換する

はじめに

WhitespaceとはBrainfuckやLazyKと同じ難解プログラミング言語と言われる言語のひとつである. 難解プログラミング言語ジョーク言語と言われるが,実装の容易さやシンプルな言語仕様を考えると,とても興味深い言語である.

Brainfuckチューリングマシンに毛がはえた程度の言語であるが,Whitespaceはスタックマシン型の処理系を想定した言語である. そして,スタック操作,算術命令,ヒープ操作,フロー制御,入出力機能を持ち,比較的高い水準でのプログラミングが可能である.

BrainfuckC言語に変換するという例は世の中にあふれ返っており,変換するコードをいくつも見掛けた. 天下のWikipediaにも変換方法は記述されている.

命令 対応するC言語コード
> ptr++;
< ptr--;
+ (*ptr)++;
- (*ptr)--;
. putchar(*ptr);
, *ptr = getchar();
[ while (*ptr) {
] }

しかし,WhitespaceをC言語に変換するという例は見掛けない. そこで,今回はWhitespaceをいかにC言語に変換するかについて書こうと思う.

Whitespaceの言語仕様

まず,Whitespaceの言語仕様を復習しようと思う. どうも簡単にググって出てきた日本語記事で紹介されているWhitespaceの仕様は少し古いものが多く,公式サイトで紹介されているものと異なっている. (日本語Wikipediaにも古い仕様が書かれている) 例えば,以下のサイトには,古いWhitespaceの仕様を記述していたり,古い仕様の処理系を置いてあったりする.

以下の表の命令欄には,

  • [Space] -> S
  • [Tab] -> T
  • [改行] -> L

として,Whitespaceのコードを書いてある. VM Code欄には,後半の説明で便宜的に利用するVM命令の名称と考えてもらうとよい. そして,動作欄にはその命令の動作を書いている.

命令 VM Code 動作
SS[NUMBER] STACK_PUSH 引数に指定された値をスタックにプッシュする(即値のプッシュ)
STS STACK_DUP_N スタックの上からn番目をコピーし,スタックの一番上に積む
SLS STACK_DUP スタックの上から1番目をコピーし,スタックの一番上に積む
STL STACK_SLIDE スタックの上から1番目をキープしつつ,上から2番目から(2 + 引数)番目を捨てる
SLT STACK_SWAP スタックの上から1番目と2番目を交換する
SLL STACK_DISCARD スタックの上から1番目を捨てる
TSSS ARITH_ADD スタックの上から1番目と2番目をポップし,それを加算(2番目 + 1番目)した結果を最上段にプッシュする
TSST ARITH_SUB スタックの上から1番目と2番目をポップし,それを減算(2番目 - 1番目)した結果を最上段にプッシュする
TSSL ARITH_MUL スタックの上から1番目と2番目をポップし,それを乗算(2番目 * 1番目)した結果を最上段にプッシュする
TSTS ARITH_DIV スタックの上から1番目と2番目をポップし,それを除算(2番目 / 1番目)した結果を最上段にプッシュする
TSTT ARITH_MOD スタックの上から1番目と2番目をポップし,その剰余(2番目 % 1番目)をとった結果を最上段にプッシュする
TTS HEAP_STORE スタックの上から1番目と2番目を取り出し,2番目をヒープのアドレスとし,そこに1番目の値を書き込む(heap[2番目] = 1番目
TTT HEAP_LOAD スタックの上から1番目を取り出し,1番目をヒープのアドレスとし,そのアドレスの値をスタックの最上段にプッシュする( stack_push(heap[1番目]);
LSS[LABEL] 引数に指定されたラベルを定義する
LST[LABEL] FLOW_GOSUB 引数に指定されたラベルをサブルーチンとして呼び出す(呼び出し位置を記憶してジャンプ)
LSL[LABEL] FLOW_JUMP 引数に指定されたラベル位置にジャンプする
LTS[LABEL] FLOW_BEZ スタックの上から1番目を取り出し,その値が0ならば引数に指定されたラベル位置にジャンプする
LTT[LABEL] FLOW_BLTZ スタックの上から1番目を取り出し,その値が0より小さいならば引数に指定されたラベル位置にジャンプする
LTL FLOW_ENDSUB サブルーチンの呼び出し元にジャンプする(関数のreturn)
LLL FLOW_HALT プログラムを終了する
TLSS IO_PUT_CHAR スタックの上から1番目を取り出し,その数値に対応するascii文字を出力
TLST IO_PUT_NUM スタックの上から1番目を取り出し,その数値をascii文字として出力
TLTS IO_READ_CHAR スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から1文字読み込む( heap[1番目] = gethar();
TLTT IO_READ_NUM スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から数値を読み込む( scanf("%d", &heap[1番目]);

上記の表を見てわかるように,命令はスタック操作,算術命令,ヒープ操作,フロー制御,入出力の5種類に大別でき,それぞれの命令毎にプレフィックスが決まっている.

命令の種類 プレフィックス
スタック操作 S
算術命令 TS
ヒープ操作 TT
フロー制御 L
入出力 TL

よく使う命令ほど,少ない文字で済むように文字が割り当てられている.

Whitespace処理系では,後方へのラベルジャンプをサポートする必要があるため,一度コード全体コンパイルをすることによって,ラベルが定義されている位置を知っておかなければならない. (コンパイル時にラベルの定義は処理するので,「ラベルを定義する」というVM命令は必要ない)

Whitespaceにおける「引数」とは,正規表現[ \t]\+$ にマッチするものと考えておくとよい. 数値はSを0,Tを1とした2進数表記で表現する. 例えば,STTST ならば,2進数: 01101 なので,数値13となる. ラベルはSとTの連続させて表現する.8文字用いて1文字のasciiコードで表現する必要はないが,そのようにしているサンプルコードもある.

Whitespace to C

WhitespaceからC言語に変換するにあたって,サブルーチン呼び出しが問題となった(他の命令については,解説する必要はないだろう). サブルーチン呼び出し(FLOW_GOSUB)はgoto(FLOW_JUMP)と違い,呼び出し元を記憶し,呼び出し終了時(FLOW_ENDSUB)に,記憶していた呼び出し元に戻る必要がある. つまり,C言語でいうところの関数呼び出しを実現しなければならない.

Brainfuckのループのように,ジャンプ先と戻り位置が1対1で対応しているならば問題はないが,サブルーチンは複数箇所から呼び出されるため,戻り位置の候補が複数あることになる. また,Whitespaceには「サブルーチン定義」という命令はなく,サブルーチン呼び出しは単なる呼び出し位置を記憶したラベルジャンプにすぎない. ジャンプ先のラベルがただのジャンプ命令に利用される可能性もあるし,ラベルより前の処理から継続して,ラベル内の処理に突入する可能性もある. そのため,サブルーチンに対応する関数を定義するという手段も使えない. 以上の理由により,サブルーチン呼び出しを単純な方法でC言語に変換することはできない.

そこで, gotosetjmp()longjmp() を用いることにより,サブルーチン呼び出しを実現した. setjmp() は簡単に言うと,呼び出し位置の情報を,引数の jmp_buf 型の変数に保存する関数である. longjmp() は,引数に指定された jmp_buf 位置に戻る. これと goto を組み合わせることで,簡易的なサブルーチン呼び出しが実現できるというわけだ.

具体的には,次のコードのようにC言語コードに変換する.

/* ... */
printf("static jmp_buf call_stack[CALL_STACK_SIZE];\n");
printf("static size_t stack_idx = 0;\n");

/* for (vmcodes : vmcode) { のようなコードでループする */
  switch (vmcode) {
    /* ... */
    case FLOW_GOSUB:
      /* LABEL = read_label(); のような感じでLABELを用意しておく */
      printf(
        "  if (!setjmp(call_stack[call_stack_idx++])) {\n"
        "    goto %s;\n"
        "  }\n", LABEL);
      break;
    case FLOW_ENDSUB:
      printf("  longjmp(call_stack[--call_stack_idx], 1);");
      break;
      /* ... */
  }
/* } */

これでサブルーチン呼び出しを実現できた. ラベルに関しては,SpaceをS,TabをTに置換したラベル名で変換先のC言語コード中に定義する.

SSSTSSTT:

まとめ

WhitespaceからC言語への変換は,サブルーチン呼び出しが鬼門であったが, gotosetjmp()longjmp() を用いることで実現できた.

なお,インタプリタ/C言語へのトランスレータの機能を持つWhitespace処理系は,koturn/Whitespaceに置いてある.

$ make

とすれば簡単にビルドできる. MSVCであっても,

> nmake /f msvc.mk

とすればビルドできるはずだ. そして,以下のように実行すると,WhitespaceからC言語へ変換できる.

$ ./whitespace [Whitespace source file] -t -o out.c

WhitespaceからC言語での変換例

最後に,koturn/Whitespaceを用いた変換例を記載する. このプログラムはWhitespaceのインタプリタ,Whitespaceコードのニーモニック表現,およびC言語ソースへの変換機能を有している. ここで,何となくWhitespaceからC言語への変換のイメージが掴めると思う.

なお,Whitespaceのコードは視認できないので,前述と同じように

  • [Space] -> S
  • [Tab] -> T

とする. ただし,改行文字はそのまま改行を行う文字とする.

変換元のWhitespaceソースコード

SSSS
SSSTSSTSSS
TTSSSST
SSSTTSSTST
TTSSSSTS
SSSTTSTTSS
TTSSSSTT
SSSTTSTTSS
TTSSSSTSS
SSSTTSTTTT
TTSSSSTST
SSSTSTTSS
TTSSSSTTS
SSSTSSSSS
TTSSSSTTT
SSSTTTSTTT
TTSSSSTSSS
SSSTTSTTTT
TTSSSSTSST
SSSTTTSSTS
TTSSSSTSTS
SSSTTSTTSS
TTSSSSTSTT
SSSTTSSTSS
TTSSSSTTSS
SSSTSSSSS
TTSSSSTTST
SSSTTSTTTT
TTSSSSTTTS
SSSTTSSTTS
TTSSSSTTTT
SSSTSSSSS
TTSSSSTSSSS
SSSTTTSSTT
TTSSSSTSSST
SSSTTTSSSS
TTSSSSTSSTS
SSSTTSSSST
TTSSSSTSSTT
SSSTTSSSTT
TTSSSSTSTSS
SSSTTSSTST
TTSSSSTSTST
SSSTTTSSTT
TTSSSSTSTTS
SSSTSSSST
TTSSSSTSTTT
SSSS
TTSSSSS

STSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

STSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST




SSSTTSSSSTSTTSSTSSSTTSSTSS
TSSS
T

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST
S
STTTS
S
TSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
T
SSSSST
TSSS
S
STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

S


T

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS
S
SS
ST
TSTTTS
SSSSTSTS
TSST
TSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSS
S
STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSSSSSS
TTS
T

SSSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST
SSSTSTS
SSSTTST
T
SST
SS
T
  • 実行例
$ ./whitespace.out hworld.ws
Hello, world of spaces!

ニーモニック表現

上記Whitespaceコードを擬似的なVM命令のニーモニック表現で表現したものである. C言語への変換に用いるわけではないが,イメージを掴む助けになるだろう. なお,ジャンプは絶対アドレス指定である.

0000: STACK_PUSH 0
0005: STACK_PUSH 72
0010: HEAP_STORE
0011: STACK_PUSH 1
0016: STACK_PUSH 101
0021: HEAP_STORE
0022: STACK_PUSH 2
0027: STACK_PUSH 108
0032: HEAP_STORE
0033: STACK_PUSH 3
0038: STACK_PUSH 108
0043: HEAP_STORE
0044: STACK_PUSH 4
0049: STACK_PUSH 111
0054: HEAP_STORE
0055: STACK_PUSH 5
0060: STACK_PUSH 44
0065: HEAP_STORE
0066: STACK_PUSH 6
0071: STACK_PUSH 32
0076: HEAP_STORE
0077: STACK_PUSH 7
0082: STACK_PUSH 119
0087: HEAP_STORE
0088: STACK_PUSH 8
0093: STACK_PUSH 111
0098: HEAP_STORE
0099: STACK_PUSH 9
0104: STACK_PUSH 114
0109: HEAP_STORE
0110: STACK_PUSH 10
0115: STACK_PUSH 108
0120: HEAP_STORE
0121: STACK_PUSH 11
0126: STACK_PUSH 100
0131: HEAP_STORE
0132: STACK_PUSH 12
0137: STACK_PUSH 32
0142: HEAP_STORE
0143: STACK_PUSH 13
0148: STACK_PUSH 111
0153: HEAP_STORE
0154: STACK_PUSH 14
0159: STACK_PUSH 102
0164: HEAP_STORE
0165: STACK_PUSH 15
0170: STACK_PUSH 32
0175: HEAP_STORE
0176: STACK_PUSH 16
0181: STACK_PUSH 115
0186: HEAP_STORE
0187: STACK_PUSH 17
0192: STACK_PUSH 112
0197: HEAP_STORE
0198: STACK_PUSH 18
0203: STACK_PUSH 97
0208: HEAP_STORE
0209: STACK_PUSH 19
0214: STACK_PUSH 99
0219: HEAP_STORE
0220: STACK_PUSH 20
0225: STACK_PUSH 101
0230: HEAP_STORE
0231: STACK_PUSH 21
0236: STACK_PUSH 115
0241: HEAP_STORE
0242: STACK_PUSH 22
0247: STACK_PUSH 33
0252: HEAP_STORE
0253: STACK_PUSH 23
0258: STACK_PUSH 0
0263: HEAP_STORE
0264: STACK_PUSH 0
0269: FLOW_GOSUB 282
0274: FLOW_GOSUB 367
0279: FLOW_HALT
0280: ARITH_ADD
0281: FLOW_ENDSUB
0282: STACK_DUP_N 0
0287: HEAP_LOAD
0288: STACK_DUP_N 0
0293: FLOW_BEZ 310
0298: IO_PUT_CHAR
0299: STACK_PUSH 1
0304: ARITH_ADD
0305: FLOW_JUMP 282
0310: STACK_POP
0311: STACK_POP
0312: FLOW_ENDSUB
0313: STACK_DUP_N 0
0318: STACK_DUP_N 0
0323: IO_READ_CHAR
0324: HEAP_LOAD
0325: STACK_DUP_N 0
0330: STACK_PUSH 10
0335: ARITH_SUB
0336: FLOW_BEZ 353
0341: STACK_POP
0342: STACK_PUSH 1
0347: ARITH_ADD
0348: FLOW_JUMP 313
0353: STACK_POP
0354: STACK_PUSH 1
0359: ARITH_ADD
0360: STACK_PUSH 0
0365: HEAP_STORE
0366: FLOW_ENDSUB
0367: STACK_PUSH 10
0372: STACK_PUSH 13
0377: IO_PUT_CHAR
0378: IO_PUT_CHAR
0379: FLOW_ENDSUB

変換後のC言語ソースコード

#include <assert.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef __cplusplus
#  if defined(_MSC_VER)
#    define inline      __inline
#    define __inline__  __inline
#  elif !defined(__GNUC__) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L)
#    define inline
#    define __inline
#  endif
#endif

#define STACK_SIZE 65536
#define HEAP_SIZE 65536
#define CALL_STACK_SIZE 65536

#define LENGTHOF(array) (sizeof(array) / sizeof((array)[0]))
#define SWAP(type, a, b) \
  do { \
    type __tmp_swap_var__ = *(a); \
    *(a) = *(b); \
    *(b) = __tmp_swap_var__; \
  } while (0)

inline static int  pop(void);
inline static void push(int e);
inline static void dup_n(size_t n);
inline static void slide(size_t n);
inline static void swap(void);
inline static void arith_add(void);
inline static void arith_sub(void);
inline static void arith_mul(void);
inline static void arith_div(void);
inline static void arith_mod(void);
inline static void heap_store(void);
inline static void heap_read(void);

static int stack[STACK_SIZE];
static int heap[HEAP_SIZE];
static jmp_buf call_stack[CALL_STACK_SIZE];
static size_t stack_idx = 0;
static size_t call_stack_idx = 0;


int main(void)
{
  push(0);
  push(72);
  heap_store();
  push(1);
  push(101);
  heap_store();
  push(2);
  push(108);
  heap_store();
  push(3);
  push(108);
  heap_store();
  push(4);
  push(111);
  heap_store();
  push(5);
  push(44);
  heap_store();
  push(6);
  push(32);
  heap_store();
  push(7);
  push(119);
  heap_store();
  push(8);
  push(111);
  heap_store();
  push(9);
  push(114);
  heap_store();
  push(10);
  push(108);
  heap_store();
  push(11);
  push(100);
  heap_store();
  push(12);
  push(32);
  heap_store();
  push(13);
  push(111);
  heap_store();
  push(14);
  push(102);
  heap_store();
  push(15);
  push(32);
  heap_store();
  push(16);
  push(115);
  heap_store();
  push(17);
  push(112);
  heap_store();
  push(18);
  push(97);
  heap_store();
  push(19);
  push(99);
  heap_store();
  push(20);
  push(101);
  heap_store();
  push(21);
  push(115);
  heap_store();
  push(22);
  push(33);
  heap_store();
  push(23);
  push(0);
  heap_store();
  push(0);
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;
  }
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST;
  }
  exit(EXIT_SUCCESS);

STTSSSSTSTTSSTSSSTTSSTSS:
  arith_add();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST:
  dup_n(0);
  heap_read();
  dup_n(0);
  if (!pop()) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  putchar(pop());
  push(1);
  arith_add();
  goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  pop();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS:
  dup_n(0);
  dup_n(0);
  heap[pop()] = getchar();
  heap_read();
  dup_n(0);
  push(10);
  arith_sub();
  if (!pop()) {
    goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  pop();
  push(1);
  arith_add();
  goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS;

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  push(1);
  arith_add();
  push(0);
  heap_store();
  longjmp(call_stack[--call_stack_idx], 1);

STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST:
  push(10);
  push(13);
  putchar(pop());
  putchar(pop());
  longjmp(call_stack[--call_stack_idx], 1);

  return EXIT_SUCCESS;
}


inline static int pop(void)
{
  assert(stack_idx < LENGTHOF(stack));
  return stack[--stack_idx];
}


inline static void push(int e)
{
  assert(stack_idx < LENGTHOF(stack));
  stack[stack_idx++] = e;
}


inline static void dup_n(size_t n)
{
  assert(n < stack_idx && stack_idx < LENGTHOF(stack) - 1);
  stack[stack_idx] = stack[stack_idx - (n + 1)];
  stack_idx++;
}


inline static void slide(size_t n)
{
  assert(stack_idx > n);
  stack[stack_idx - (n + 1)] = stack[stack_idx - 1];
  stack_idx -= n;
}


inline static void swap(void)
{
  assert(stack_idx > 1);
  SWAP(int, &stack[stack_idx - 1], &stack[stack_idx - 2]);
}


inline static void arith_add(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] += stack[stack_idx];
}


inline static void arith_sub(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] -= stack[stack_idx];
}


inline static void arith_mul(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] *= stack[stack_idx];
}


inline static void arith_div(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] /= stack[stack_idx];
}


inline static void arith_mod(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] %= stack[stack_idx];
}


inline static void heap_store(void)
{
  int value = pop();
  int addr  = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  heap[addr] = value;
}


inline static void heap_read(void)
{
  int addr = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  push(heap[addr]);
}
$ ./whitespace.out hworld.ws -t -o hworld.c
$ gcc -Ofast -march=native -DNDEBUG hworld.c -o hworld.out
$ ./hworld.out
Hello, world of spaces!

Wandboxでの実行結果はこちら

参考