先日、「ウ」と「ホ」と改行文字だけでプログラミングできる「ゴリラ言語」を作りました。
そういえば先日、「ウ」と「ホ」と改行だけでプログラミング出来るゴリラ言語作ったのでソース置いておきますね。「ウホッ」でもいいよ。https://t.co/XcXFcvDhRy
— mattn (@mattn_jp) March 24, 2020
コード短いのでそのまま張っておきます。
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
)
type Op int
const (
Push Op = iota
Signed
Dup
Copy
Swap
Discard
Slide
Add
Sub
Mul
Div
Mod
Store
Retrieve
Mark
Unsigned
Call
Jump
Jz
Jn
Ret
Exit
OutC
OutN
InC
InN
Nop
)
type opcode struct {
code Op
arg int
}
type opcodes []opcode
func (p *opcodes) jmp(arg int, pc *int) {
for i, t := range *p {
if t.code == Mark && t.arg == arg {
*pc = i
}
}
}
var optable = map[string]Op{
" ": Push,
" \n ": Dup,
" \n\t": Swap,
" \n\n": Discard,
"\t ": Add,
"\t \t": Sub,
"\t \n": Mul,
"\t \t ": Div,
"\t \t\t": Mod,
"\t\t ": Store,
"\t\t\t": Retrieve,
"\n ": Mark,
"\n \t": Call,
"\n \n": Jump,
"\n\t ": Jz,
"\n\t\t": Jn,
"\n\t\n": Ret,
"\n\n\n": Exit,
"\t\n ": OutC,
"\t\n \t": OutN,
"\t\n\t ": InC,
"\t\n\t\t": InN,
//" \t ": Copy,
//" \t\n": Slide,
}
type stack []int
func (s *stack) push(v int) {
*s = append(*s, v)
}
func (s *stack) pop() int {
l := len(*s)
if l == 0 {
return -1
}
v := (*s)[l-1]
*s = (*s)[:l-1]
return v
}
func (s *stack) dup() {
l := len(*s)
if l == 0 {
return
}
v := (*s)[l-1]
*s = append(*s, v)
}
func uho(src []rune) {
for i, r := range src {
switch r {
case 'ウ':
src[i] = ' '
case 'ホ':
src[i] = '\t'
}
}
// remove needless comments
for {
pos := strings.IndexFunc(string(src), func(r rune) bool {
return r != ' ' && r != '\t' && r != '\n'
})
if pos < 0 {
break
}
if pos == 0 {
src = src[1:]
} else {
src = append(src[:pos], src[pos+1:]...)
}
}
// parse uho into tokens
tokens := opcodes{}
for len(src) > 0 {
op := ""
code := Nop
for k, v := range optable {
if strings.HasPrefix(string(src), k) {
op = k
code = v
break
}
}
if op == "" {
src = src[1:]
continue
}
src = src[len(op):]
var arg int
switch code {
case Push:
// handle argument
handle_signed_arg:
for i := 1; i < len(src); i++ {
switch src[i] {
case ' ':
arg = (arg << 1) | 0
case '\t':
arg = (arg << 1) | 1
case '\n':
// Push take singed argument
if src[0] == '\t' {
arg = -arg
}
src = src[i+1:]
break handle_signed_arg
}
}
case Mark, Call, Jump, Jz, Jn:
// handle argument
handle_unsigned_arg:
for i := 0; i < len(src); i++ {
switch src[i] {
case ' ':
arg = (arg << 1) | 0
case '\t':
arg = (arg << 1) | 1
case '\n':
src = src[i+1:]
break handle_unsigned_arg
}
}
}
tokens = append(tokens, opcode{code, arg})
}
pc := 0
ps := stack{}
cs := stack{}
heap := map[int]int{}
for {
token := tokens[pc]
code, arg := token.code, token.arg
//fmt.Println(pc, code, arg)
pc++
switch code {
case Push:
ps.push(arg)
case Mark:
case Dup:
ps.dup()
case OutN:
fmt.Print(ps.pop())
case OutC:
fmt.Print(string(rune(ps.pop())))
case Add:
rhs := ps.pop()
lhs := ps.pop()
ps.push(lhs + rhs)
case Sub:
rhs := ps.pop()
lhs := ps.pop()
ps.push(lhs - rhs)
case Mul:
rhs := ps.pop()
lhs := ps.pop()
ps.push(lhs * rhs)
case Div:
rhs := ps.pop()
lhs := ps.pop()
ps.push(lhs / rhs)
case Mod:
rhs := ps.pop()
lhs := ps.pop()
ps.push(lhs % rhs)
case Jz:
if ps.pop() == 0 {
tokens.jmp(arg, &pc)
}
case Jn:
if ps.pop() < 0 {
tokens.jmp(arg, &pc)
}
case Jump:
tokens.jmp(arg, &pc)
case Discard:
ps.pop()
case Exit:
os.Exit(0)
case Store:
v := ps.pop()
address := ps.pop()
heap[address] = v
case Call:
cs.push(pc)
tokens.jmp(arg, &pc)
case Retrieve:
ps.push(heap[ps.pop()])
case Ret:
pc = cs.pop()
case InC:
var b [1]byte
os.Stdin.Read(b[:])
heap[ps.pop()] = int(b[0])
case InN:
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
i, _ := strconv.Atoi(scanner.Text())
heap[ps.pop()] = i
}
case Swap:
ps[len(ps)-1], ps[len(ps)-2] = ps[len(ps)-2], ps[len(ps)-1]
default:
panic(fmt.Sprintf("Unknown opcode: %v", code))
}
}
}
func main() {
var content bytes.Buffer
for _, f := range os.Args[1:] {
b, err := ioutil.ReadFile(f)
if err != nil {
fmt.Fprintln(os.Stderr, "read file:", err)
os.Exit(1)
}
content.Write(b)
}
if content.Len() == 0 {
if _, err := io.Copy(&content, os.Stdin); err != nil {
fmt.Fprintln(os.Stderr, "read stdin:", err)
os.Exit(1)
}
}
uho([]rune(content.String()))
}
以下のコードをコマンドラインから標準入力に食わせると、FizzBuzz が実行されます。
ウウウホッ
ウウウウ
ウ
ウウ
ウウウウホホ
ホウホッホ
ホウウホ
ウウウホッウホ
ホウホッホ
ホウウホウ
ウ
ウホ
ウホッ
ウ
ウホウウ
ウウウホッ
ウウウホッウウウホホウ
ホ
ウウウウウホッホウホッウウホ
ホ
ウウ
ウホッウホホ
ウウウホッウホ
ホウホッホ
ホウウホウ
ウ
ウホウウ
ウウウホッウ
ウウウホウウウウホッウ
ホ
ウウウウウホッホホウホウホッ
ホ
ウウ
ウホウホッホ
ウ
ウホウウ
ウウウホホ
ウウウホッホホホウホッウ
ウ
ウホッ
ウウホ
ウウ
ホ
ウウウホッウウ
ウウウホウホッウ
ホ
ウウウウウホ
ホウウウウ
ウウウウホッホウウホッウホ
ホウウホ
ホホウウ
ウ
便利ですね。また以下のコードを食わせると「ウホッ!」と表示されます。
ウッウウ
ウウウホウホウ
ウウウッホッウウウウホ
ウウウッホホウウウウッホホウッウウウッホホ
ウウウホホウウウッウホホウホッホウホホ
ウウウッホッホウウウウッホウホウウホホウ
ウウホウウウッウーウウウウウホウッウウホウウーホウウ
ウ
ウ
ホウッホホホウホウウウッホウホウウ
ホ
ウウ
ウ
ホウウウッウウウウウウッホウウーウホウウホウウ
ウウホホホウホウウウホッウホウウ
ウ
勘のいい方はコード見なくてもこの説明だけで気付いたかもしれません。そう「whitespace」です。
Whitespace(ホワイトスペース)は、プログラミング言語のひとつであり、またそれを動作させるインタプリタを指している。WhitespaceはGPLにより配布されている。実用言語ではない難解プログラミング言語のひとつ。
本来 "whitespace" とは「空白」や「余白」を意味する英単語である。多くの一般的なプログラミング言語では空白に相当する文字(スペース、タブ、言語によっては改行も)は他の言語要素間の区切りとして使われている。しかし、言語 Whitespace においてはプログラムは空白文字だけで構成される(それ以外の文字列はコメント扱いで無視される)。そのため、一見するとプログラムであることすらわからないという珍しい言語である。
ゴリラ言語は、空白を「ウ」、タブ文字を「ホ」に見立てた Esolang です。この言語で目的の文字を表示するコードをどうやって書いているか想像もつかないかもしれません。実は whitespace のコンパイラを作っておられる方がいます。
ウェブ上の IDE になっています。例えばここの Files の中にある nerd.wsa
を開くと以下のコードが表示されます。
; Push the zero-terminated string
; onto stack in reverse order.
push "Hello Nerd!\n"
; Loop while top of stack is non-zero.
print:
dup
jz end
printc
jmp print
; Graceful end
end:
drop
end
独自のアセンブラ言語になっていて、コンパイルすると whitespace のソースが出力されます。この Hello Nerd!
を ウホッ!
書き換えると空白とタブ文字と改行文字で作られる whitespace のソースコードが出来上がります。
真っ白けですね。vim 等を使い、空白を「ウ」、タブ文字を「ホ」に置換してみます。
ウウウ
ウウウホウホウ
ウウウホホホホホホホホウウウウウウウホ
ウウウホホウウウウホホウウウウホホ
ウウウホホウウウウホホウホホウホホ
ウウウホホウウウウホウホウウホホウ
ウウホウウウウウウウウウホウウウホウウホウウ
ウ
ウ
ホウホホホウホウウウホウホウウ
ホ
ウウ
ウ
ホウウウウウウウウウホウウウホウウホウウ
ウウホホホウホウウウホウホウウ
ウ
読めそうな気がしてきましたね。読める気がしましたよね?
読んでみましょう。ゴリラ言語の仕様は以下の通り。
IMP
[ウ] スタック操作
[ホ][ウ] 演算
[ホ][ホ] ヒープアクセス
[LF] フロー制御
[ホ][LF] I/O
STACK OPERATION
[ウ] 数値:数値をスタックに積む
[LF][ウ]:スタックの一番上を複製する
[ホ][ウ] 数値:スタックのn番目をコピーして一番上に積む
[LF][ホ]:スタックの1番目と2番目を交換する
[LF][LF]:スタックの一番上の物を捨てる
CALCULATE
[ウ][ウ]:加算
[ウ][ホ]:引き算
[ウ][LF]:かけ算
[ホ][ウ]:割り算
[ホ][ホ]:剰余
始めはデータ部ですね。push ([ウ])に続く引数、0(なし), 10(1<<1, 2<<1, 5<<1), 65281(1<<1, 2<<1, 4<<1, 8<<1, 16<<1, 33<<1), 12483(略), 12507(略), 12454(略) を積んでいます。スタックですから取り出す時には逆から取り出されます。つまりこれは「12454, 12507, 12483, 65281, 10, 0」、文字に直すと「ウホッ!」です。続いて制御部
ウウホウウウウウウウウウホウウウホウウホウウ
ウ
ウ
ホウホホホウホウウウホウホウウ
ホ
ウウ
ウ
ホウウウウウウウウウホウウウホウウホウウ
ウウホホホウホウウウホウホウウ
ウ
マーク([LF][ウ][ウ])でラベル定義([ホ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ウ][ホ][ウ][ウ][ウ][ホ][ウ][ウ][ホ][ウ][ウ])、スタックから取り出し([ウ][LF][ウ])、ゼロ比較([LF][ホ][ウ])、0 であればラベル ホホホウホウウウホウホウウ
にジャンプしています。0 でない場合は画面に取り出した値を文字として表示([ホ][LF][ウ])します。そしてループの最初に戻る為に ホウウウウウウウウウホウウウホウウホウウ
へジャンプ([LF][ウ][LF])です。0 比較時のジャンプ先としてマーク([LF][ウ][ウ])でラベル定義([ホ][ホ][ホ][ウ][ホ][ウ][ウ][ウ][ホ][ウ][ホ][ウ][ウ])しています。
ループから出た後は、スタックの一番上を捨て([ウ][LF][LF])、プログラムを終了([LF][LF][LF])します。
簡単ですね!もうこれでいつ会社で「先輩、このゴリラ言語わからないので読んでもらえますか?」と聞かれても読めますよね。
ちなみにこれを実行すると以下の様に画面に表示されます。
言ってしまえば whitespace のパクリ言語なのですが、2文字書き換えるだけでオレオレ言語が作れます。「ピ」と「ヨ」と改行でヒヨコ言語を作る事も出来ます。ぜひ楽しんで下さい。