こんにちは。mds_boyです。最近はRustとScalaを書いています。
本記事は、traP AdventCalender43日目の記事です。
Pxemって?
さて、今回はいわゆる難解プログラミング言語の一種である「Pxem」の紹介をしていきたいと思います。
Pxemは、タイトルにも書いた通りソースコードの中身は空っぽで動く言語です。
え?中身が空でどうやってコーディングするの🤔?
答えは簡単、ファイル名がプログラムの一部になっています。つまりファイル名を色々変えてプログラムを組むわけです。
なるほどね、その手があったか……
元々はコードゴルフ向けに作られた[1]そうです。
Hello,world
実際に例を見ていきましょう。
C++で書かれたインタプリタをコンパイルします。
$ g++ -o Pxem pxemInterpreter.cpp
そして中身が空のHello,world!.pxeというファイルを作成します。作成方法は何でもいいですが、以下ではtouch
コマンドを使います。拡張子は.pxeまたは.pxemとされていますが、多分何でも動きます。
これを実行すると
$ touch Hello,world!.pxe
$ ./Pxem Hello,world!.pxe
Hello,world!
見事Hello,worldできました。やったね。
機能紹介
Pxemは、int値をスタックにpushしたりpopすることで処理を行います。
コマンドは「.(一文字)」で構成されます。
コマンドでない部分は文字コードの列として逆順に積まれていきます。スタックはLIFO(後入れ先出し)なので、結果として文字を前から取り出すことができます。
以下、コマンドのうちいくつかを紹介します。
出力
コマンド | 機能 |
---|---|
.p |
スタックが空になるまでpopし文字列として出力 |
.o |
スタックから1つpopし文字として出力 |
.n |
スタックから1つpopし数値として出力 |
先程のHello,world!.pxeなら、Hello,world!
までの部分が逆の!
からH
という順に積まれ、.p
で全てpopして文字列として出力されます。
次に、.o
の例です。
$ touch Hello.oworld!.pxe
$ ./Pxem Hello.oworld!.pxe
Hworldello!
Hello
まで逆順に積まれ、.o
で先頭のH
のみ出力されます。この時点でスタックに積まれた文字はello
です(eが先に出力される)。ここに後からworld!
が積まれます。.p
で後から入れたものが先にpopされ、worldello!
が出力されます。
.n
の例です。(以下、文字コードをASCIIコードと仮定します)
$ touch 1.n.pxe
$ ./Pxem 1.n.pxe
49
文字'1'
に対応する数値49
が出力されます。(以後、区別するため文字としての数値を''
で表記)
四則演算
コマンド | 機能 |
---|---|
.+ | スタックから2つpopし足してpush |
.- | スタックから2つpopし、大きい方から小さい方を引いてpush |
.! | スタックから2つpopし掛けてpush |
.$ | スタックから2つpopし、大きい方から小さい方を割った商をpush |
.% | スタックから2つpopし、大きい方から小さい方を割った余りをpush |
$ touch ak.-.n.pxe
$ ./Pxem ak.-.n.pxe
10
k
、a
の順でスタックにpushし、.-
でa
、k
の順に取り出されて文字コードを引いた10
がpushされます。.n
で10
が出力されます。
$ touch ak.-.p.pxe
$ ./Pxem ak.-.p.pxe
次に.p
で10
を文字コードとして出力してみると、改行\n
になりました。これは後で使います。
制御
コマンド | 機能 |
---|---|
.a | 終止コマンド。対応する.w/.x/.y/.zのどれかに戻る |
.w | スタックから1つpopし0なら対応する.aにジャンプ |
.x | スタックから2つpopし、(先)>=(後)ならループを終了して.aにジャンプ |
.y | スタックから2つpopし、(先)<=(後)ならループを終了して.aにジャンプ |
.z | スタックから2つpopし、(先)==(後)ならループを終了して対応する.aにジャンプ |
$ touch ab.xpo.p.a.pxe
$ ./Pxem ab.xpo.p.a.pxe
popopopopopopopopopopopopopopopopopopopopopopopopopopopopopo....
まずab
を逆順にpushし、xでa
、b
の順でpopします。a
<b
なのでループの処理内容po.p
を実行します。.a
で対応する.x
に戻り、処理を繰り返します。
スタック
コマンド | 機能 |
---|---|
.c | スタックから1つpopしコピーして2つpush |
.s | スタックから1つpopして値を捨てる |
$ touch Hello.s.c.pxe
$ ./Pxem Hello.s.c.pxe
eello
まずHello
を逆順にpushします。.s
で先頭のH
が捨てられます。.c
で次のe
が増えます。出力するとeello
になります。
一時領域
一時領域とは、スタックとは別で値を保存できる場所のことです。保存できる値は1つで、新しい値が古い値を上書きします。
コマンド | 機能 |
---|---|
.t | スタックから1つpopし一時領域に保存 |
.m | 一時領域の値をスタックにpush |
$ touch Hello.t.p.m.pxe
$ ./Pxem Hello.t.p.m.pxe
elloH
まずHello
を逆順にpushします。.t
で先頭のH
がpopされ一時領域に保存されます。.p
でello
が出力されます。.m
でH
がスタックにpushされ、出力されます。
その他
コマンド | 機能 |
---|---|
.d | 実行を終了する |
FizzBuzz
さて、プログラミング言語を始めたらまず書いてみるのがFizzBuzzということで、100までのFizzBuzzを考えてみましょう。
ここまで紹介したコマンドでできます。
はい、わかりませんね。答えはこれらしい[2]です。
ak.-akbuzz.-ak4.-akfizz.-ak2.-1.p05.-.tab.z01.-.c.m.+.c.t05.-.%.w.s01.-.m03.-.%.W.s.m.nak.-.p00.-.c.c.c.a.wak.-fizz.p00.-.c.c.a.a.w01.-.m03.-.%.w.sak.-buzz.p00.-.c.c.a.wak.-fizzbuzz.p00.-.c.a.a.md2.-02.-.!.a.d.pxe
解説していこうと思います。
先頭から見ていきます。ak.-
は先程の通り改行\n
です。さらにa
とk
、buzz
がpushされます。
ak.-akbuzz
時点:a
|k
|b
|u
|z
|z
|\n
(前が先にpopされる)
次の.-
では、先頭の2つa
とk
からまた\n
がpushされます。さらにa
、k
、'4'
がpushされます。
ak.-akbuzz.-ak4
時点:a
|k
|'4'
|\n
|b
|u
|z
|z
|\n
次の.-
でも2つpopして\n
がpushされ、fizz
がpushされます。
ak.-akbuzz.-ak4.-akfizz
時点:a
|k
|f
|i
|z
|z
|\n
|'4'
|\n
|b
|u
|z
|z
|\n
次の.-
でも2つpopして\n
がpushされ、2
がpushされます。
ak.-akbuzz.-ak4.-akfizz.-ak2
時点:a
|k
|'2'
|\n
|f
|i
|z
|z
|\n
|'4'
|\n
|b
|u
|z
|z
|\n
次の.-
でも2つpopして\n
がpushされ、'1'
がpushされます。.p
で出力します。
ak.-akbuzz.-ak4.-akfizz.-ak2.-1.p
時点:|'1'
|\n
|'2'
|\n
|f
|i
|z
|z
|\n
|'4'
|\n
|b
|u
|z
|z
|\n
ここまでで実行してみると以下のようになります。
$ touch ak.-akbuzz.-ak4.-akfizz.-ak2.-1.pxe
$ ./Pxem ak.-akbuzz.-ak4.-akfizz.-ak2.-1.pxe
1
2
fizz
4
buzz
お、1~5までのFizzBuzzが出来てますね!確かに前から見た通り出力されています。
.p
で出力してスタックの中身が一旦空になるので、その先だけを考えましょう。
まず05.-
で5
がpushされます。.t
で5
を一時領域に格納します。そしてa
、b
をpushします。
05.-.tab
時点:a
|b
(一時領域:5
)
さて、.z
でループが始まります。このループは最後まで続くループです。ループの深さを可視化してみましょう。
.z01.-.c.m.+.c.t05.-.%
.w.s01.-.m03.-.%
.W.s.m.nak.-.p00.-.c.c.c
.a
.wak.-fizz.p00.-.c.c
.a
.a
.w01.-.m03.-.%
.w.sak.-buzz.p00.-.c.c
.a
.wak.-fizzbuzz.p00.-.c
.a
.a.md2.-02.-.!
.a.d.pxe
いやわからんが……
はい、まず.z
で先頭2つが等しくなるまで処理を行います。最初a
とb
は等しくないので進みます。01.-
で1
をpushします。これを.c
で複製し、.m
で一時領域の5
をスタックにpushします。
05.-.tab.z01.-.c.m
時点:5
|1
|1
(一時領域:5
)
次に.+
で前2つを足して6
をpushします。この6
が、現在判定しようとしている値です。.c
で複製し、.t
で1つを一時領域に格納します。そして05.-
で5
をpushします。.%
で6
を5
で割った余り1
をpushします。
05.-.tab.z01.-.c.m.+.c.t05.-.%
時点:1
|1
(一時領域:6
)
.w
で先頭が0でない限り処理を行います。つまり、現在判定しようとしている数が5で割り切れない場合は以降の処理を行うということです。.s
でスタックを空にします。01.-
で1
をpushし、.m
で一時領域の6
をpushし、03-
で3
をpush、.%
で6
を3
で割った余り0
をpushします。
05.-.tab.z01.-.c.m.+.c.t05.-.%.w.s01.-.m03.-.%
時点:0
|1
(一時領域:6
)
.W
(大文字と小文字で特に違いはない)で同じく先頭が0でない限り処理を行います。つまり、現在判定しようとしている数が3で割り切れない場合は以降の処理を行うということです。5でも3でも割り切れない数値は、そのまま出力します。というわけで、.s
でスタックを空にし、.m
で一時領域の値を持ってきて、.n
で数値として出力します。ak.-.p
で\n
を出力し、00.-
で0
をpush、.c
を3回行います。この0
4つで4つの.w
から脱出します。
その結果、.m
で一時領域の値6
をスタックにpushし、d2.-
で50
、02.-
で2
をpushし、.!
で掛けて100
をpushします。この100
がFizzBuzzの上限となります。
数値そのままではなくFizz
やBuzz
、FizzBuzz
を出力するときも場合分けに従って行います。
FizzBuzzの流れは大体こんな感じです。
実際に実行してみると
$ ./Pxem ak.-akbuzz.-ak4.-akfizz.-ak2.-1.p05.-.tab.z01.-.c.m.+.c.t05.-.%.w.s01.-.m03.-.%.W.s.m.nak.-.p00.-.c.c.c.a.wak.-fizz.p00.-.c.c.a.a.w01.-.m03.-.%.w.sak.-buzz.p00.-.c.c.a.wak.-fizzbuzz.p00.-.c.a.a.md2.-02.-.!.a.d.pxe
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
fizz
22
23
fizz
buzz
26
fizz
28
29
fizzbuzz
(中略)
82
83
fizz
buzz
86
fizz
88
89
fizzbuzz
91
92
fizz
94
buzz
fizz
97
98
fizz
buzz
確かにFizzBuzzが100まで動きました!
これで0バイトでFizzBuzzが書けるようになりましたね!
おわり
自分でもこういう面白いものを作りたいですね〜
明日はYataka_MLくんの記事です。楽しみ〜