feature image

2019年12月12日 | ブログ記事

ソースコードの中身0バイトで動く言語「Pxem」の紹介【AdC2019 43日目】

こんにちは。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

kaの順でスタックにpushし、.-akの順に取り出されて文字コードを引いた10がpushされます。.n10が出力されます。

$ touch ak.-.p.pxe
$ ./Pxem ak.-.p.pxe

次に.p10を文字コードとして出力してみると、改行\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でabの順で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され一時領域に保存されます。.pelloが出力されます。.mHがスタックに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です。さらにakbuzzがpushされます。
ak.-akbuzz時点:a|k|b|u|z|z|\n(前が先にpopされる)

次の.-では、先頭の2つakからまた\nがpushされます。さらにak'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されます。.t5を一時領域に格納します。そしてabを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つが等しくなるまで処理を行います。最初abは等しくないので進みます。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します。.%65で割った余り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、.%63で割った余り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回行います。この04つで4つの.wから脱出します。

その結果、.mで一時領域の値6をスタックにpushし、d2.-5002.-2をpushし、.!で掛けて100をpushします。この100がFizzBuzzの上限となります。

数値そのままではなくFizzBuzzFizzBuzzを出力するときも場合分けに従って行います。

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くんの記事です。楽しみ〜


  1. 作者による振り返り ↩︎

  2. 旧紹介ページのアーカイブ ↩︎

mds_boy icon
この記事を書いた人
mds_boy

ゲーム制作や競技プログラミングをしています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
2019年4月22日
アセンブリを読んでみよう【新歓ブログリレー2019 45日目】
eiya icon eiya
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記