ゴリラ言語の読み方

先日、「ウ」と「ホ」と改行文字だけでプログラミングできる「ゴリラ言語」を作りました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">そういえば先日、「ウ」と「ホ」と改行だけでプログラミング出来るゴリラ言語作ったのでソース置いておきますね。「ウホッ」でもいいよ。<a href="https://t.co/XcXFcvDhRy">https://t.co/XcXFcvDhRy</a></p>&mdash; mattn (@mattn_jp) <a href="https://twitter.com/mattn_jp/status/1242489664339992578?ref_src=twsrc%5Etfw">March 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

コード短いのでそのまま張っておきます。

<detail><div>

```go
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()))
}
```
</div></detail>

以下のコードをコマンドラインから標準入力に食わせると、FizzBuzz が実行されます。

```
ウウウホッ

ウウウウ
ウ
ウウ
ウウウウホホ
ホウホッホ
ホウウホ
ウウウホッウホ
ホウホッホ
ホウウホウ
ウ
ウホ
ウホッ
ウ
ウホウウ

ウウウホッ
ウウウホッウウウホホウ
ホ
ウウウウウホッホウホッウウホ
ホ
ウウ
ウホッウホホ
ウウウホッウホ
ホウホッホ
ホウウホウ

ウ
ウホウウ

ウウウホッウ
ウウウホウウウウホッウ
ホ
ウウウウウホッホホウホウホッ
ホ
ウウ
ウホウホッホ

ウ
ウホウウ

ウウウホホ
ウウウホッホホホウホッウ
ウ
ウホッ
ウウホ
ウウ
ホ

ウウウホッウウ
ウウウホウホッウ
ホ
ウウウウウホ
ホウウウウ
ウウウウホッホウウホッウホ
ホウウホ
ホホウウ
ウ


```

便利ですね。また以下のコードを食わせると「ウホッ！」と表示されます。

```
ウッウウ
ウウウホウホウ
ウウウッホッウウウウホ
ウウウッホホウウウウッホホウッウウウッホホ
ウウウホホウウウッウホホウホッホウホホ
ウウウッホッホウウウウッホウホウウホホウ

ウウホウウウッウーウウウウウホウッウウホウウーホウウ
ウ
ウ
ホウッホホホウホウウウッホウホウウ
ホ
ウウ
ウ
ホウウウッウウウウウウッホウウーウホウウホウウ

ウウホホホウホウウウホッウホウウ
ウ



```

勘のいい方はコード見なくてもこの説明だけで気付いたかもしれません。そう「whitespace」です。

https://ja.wikipedia.org/wiki/Whitespace

>Whitespace（ホワイトスペース）は、プログラミング言語のひとつであり、またそれを動作させるインタプリタを指している。WhitespaceはGPLにより配布されている。実用言語ではない難解プログラミング言語のひとつ。
>本来 "whitespace" とは「空白」や「余白」を意味する英単語である。多くの一般的なプログラミング言語では空白に相当する文字（スペース、タブ、言語によっては改行も）は他の言語要素間の区切りとして使われている。しかし、言語 Whitespace においてはプログラムは空白文字だけで構成される（それ以外の文字列はコメント扱いで無視される）。そのため、一見するとプログラムであることすらわからないという珍しい言語である。

ゴリラ言語は、空白を「ウ」、タブ文字を「ホ」に見立てた Esolang です。この言語で目的の文字を表示するコードをどうやって書いているか想像もつかないかもしれません。実は whitespace のコンパイラを作っておられる方がいます。

https://vii5ard.github.io/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 のソースコードが出来上がります。

```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])します。

簡単ですね！もうこれでいつ会社で「先輩、このゴリラ言語わからないので読んでもらえますか？」と聞かれても読めますよね。

ちなみにこれを実行すると以下の様に画面に表示されます。

![ET48oE2UEAAPUn5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/665/3b26b272-12d0-bd48-523d-189deb4cbd48.png)

言ってしまえば whitespace のパクリ言語なのですが、2文字書き換えるだけでオレオレ言語が作れます。「ピ」と「ヨ」と改行でヒヨコ言語を作る事も出来ます。ぜひ楽しんで下さい。