はじめに
久しぶりの記事投稿、bbo51dogことびーぼです。
以前、気の赴くままに自作言語を作っていたのですが、パーサーの実装中にモチベを失ってしまい中断していました。
しかし、QiitaやTwitterを徘徊していた時にBrainf**kやWhitespaceに関する記事を目にし、これならパーサーなんてないようなものだし簡単なのでは!と思って自作難解プログラミング言語、"BboLang"を作ってみることにしました。全て自己流かつ適当なのでおかしな所だらけだと思いますが悪しからず。
コードはこちら
概要
使用した言語は最近触り始めたNimです。
文法は私の名前"bbo"を元に、"b","o","B","O"の4文字のみで構成されます。
適当なスタックマシンの仮想マシンを作って、インタプリタ(のようなもの)を作ります。中身はアセンブリやWhitespaceを10000倍単純にしたようなものです。
また、BboLangのファイルは**.bbolang
という安直で長すぎる拡張子を使うことにしてみました。
文法
以前書いたREADMEからそのまま流用します。
b
o
B
O
の4文字のみで表記する。大文字小文字の区別あり。
スペース・改行などは全て無視して何も意味を持たないため、命令ごとに改行して表記するかどうか、パラメータと命令の間に空白を入れるかどうかは自由。
命令一覧
Name | OpCode | Param | Detail |
---|---|---|---|
ADD | bbbb | - | スタックの(1番目)+(2番目) |
SUB | bbbo | - | スタックの(1番目)-(2番目) |
MUL | bbob | - | スタックの(1番目)*(2番目) |
DIV | bboo | - | スタックの(1番目)/(2番目) |
PUSH | bobb | 数値リテラル | スタックにプッシュ |
POP | bobo | - | スタックトップを破棄 |
ECHO_CHAR | oobb | - | スタックトップを文字として標準出力(数値をASCIIコードで文字に変換) |
ECHO_INT | oobo | - | スタックトップを数字として標準出力 |
リテラル
B
で囲うとリテラルを表す。
数値リテラル
b
が1、o
が0を表し、2進数で表記。
(例) BboobB
=> 1001
=> 9
実装
VMを作っていきましょう。とりあえず情報を保持するためにオブジェクトを。
import streams
type
Stack = ref object
values: seq[int]
VirtualMachine = ref object
stack: Stack
stream: Stream
proc newVirtualMachine*(stream: Stream): VirtualMachine =
new result
result.stack = Stack()
result.stream = stream
streamsは渡されたコードを1文字ずつ読んでいくためのヤツです。ファイルを渡されたのか文字列なのかに依存しないようにStreamを持ってますが現状FileStreamしか使ってません。
Stackは名前の通りスタックです。実体はintのシーケンスを持ってるだけです(なのでスタックにはintを積むだけです)。このままではスタックに何もできないので操作を追加してみましょう。
proc push(stack: Stack, value: int) =
stack.values.add(value)
proc pop(stack: Stack): int =
stack.values.pop
はい、単純明快ですね。pushでシーケンスの一番後ろに追加してます。popはシーケンスに元々popがあったのでそれを呼んでるだけです。
では、スタックはこれで完成ということにしてVMの命令の実装に移りましょう。
上記の文法からも分かるとおり、命令は四則演算とpush、pop、出力の全8通りのみです。まずはその命令たちを落とし込みましょう。
type
OpCode {.pure.} = enum
Add
Sub
Mul
Div
Push
Pop
EchoChar
EchoInt
列挙型で全ての命令のオペコードを並べました。これだけでは意味がないのでオペコードを受け取って処理をする関数を作ります。
proc exec(vm: VirtualMachine, op: OpCode) =
case op
of OpCode.Add:
let x = vm.stack.pop
let y = vm.stack.pop
vm.stack.push(x + y)
of OpCode.Sub:
let x = vm.stack.pop
let y = vm.stack.pop
vm.stack.push(x - y)
of OpCode.Mul:
let x = vm.stack.pop
let y = vm.stack.pop
vm.stack.push(x * y)
of OpCode.Div:
let x = vm.stack.pop
let y = vm.stack.pop
vm.stack.push(int(x / y))
of OpCode.Push:
# 後述
of OpCode.Pop:
discard vm.stack.pop
of OpCode.EchoChar:
stdout.write(char(vm.stack.pop))
of OpCode.EchoInt:
stdout.write($vm.stack.pop)
先ほど実装したpushやpopを使って実装してみました。一つ一つは解説しないので読み取ってください()
しかし、pushは数値をパラメータに取るため、このままでは書けません。そこで、数値リテラルをStreamから読み取る部分を先に書きます。が、予期しない入力があった時のためにエラー出力用の関数も作っておきます。
import terminal
proc error*(message: string) =
stdout.styledWrite(fgRed, "Error: ", resetStyle)
stdout.writeLine(message)
quit 1
これでいい感じに色をつけてエラーを出力し、終了してくれるようになりました。
何故stderr
ではなくstdout
にしたのかは自分でも謎です。いつか気が向けば直します。
ともかく、これでエラーには対応できるようになったので今度こそ数値を読み取る部分を書きましょう。
const numSeparator = 'B'
proc readNum(vm: VirtualMachine): int =
if vm.stream.readChar != numSeparator:
error("Invalid number")
var rowNum = ""
while vm.stream.peekChar != numSeparator:
case vm.stream.readChar
of 'b':
rowNum &= $1
of 'o':
rowNum &= $0
else:
error("Invalid number")
if vm.stream.atEnd:
error("Invalid number")
discard vm.stream.readChar
fromBin[int](rowNum)
これでコードを読んでintを返すことができるようになりました。numSeparator
は一般的な言語でいう'
や"
のような扱いです。(中身は文字列ではありませんが)
内容としては1文字ずつ読み取ってbを1、oを0に変換し、最後に10進数に変換して返しています。
これで数値を読めるようになったのでexec()
関数を完成させましょう。
proc exec(vm: VirtualMachine, op: OpCode) =
case op
#略
of OpCode.Push:
vm.stack.push(vm.readNum)
#略
コードの実行部分は完成です。
続いて数値以外の部分も読み取れるようにしていきます。
まず、BboLangでは全ての空白文字を無視するので、空白文字を全てスキップできるようにします。
(ここまで書いて気付いたのですが、リテラル内の空白文字は無視できていませんでしたね。気付かなかったことにしておきます。)
proc skipWhiteSpace(stream: Stream) =
while stream.peekStr(1).isEmptyOrWhitespace:
discard stream.readChar
はい、空白文字を飛ばせるようになりました。
続いてオペコードの名前と実際のオペコードを結びつけたいので、enumに手を加えます。
type
OpCode {.pure.} = enum
Add = "bbbb"
Sub = "bbbo"
Mul = "bbob"
Div = "bboo"
Push = "bobb"
Pop = "bobo"
EchoChar = "oobb"
EchoInt = "oobo"
これで文字列からOpCode
へ変換可能になりました。
ここで余談ですが、これらのオペコードは前半2文字で命令の種類がわかるようになっています。
前半2文字 | 種類 |
---|---|
bb | 四則演算 |
bo | スタック操作 |
oo | 出力 |
後半2文字は重複を避けるただの識別子のようなものです。
では本題に戻ってオペコードを読み取りましょう。
proc readOpcode(vm: VirtualMachine): OpCode =
var rawCode = ""
for i in 1..4:
vm.stream.skipWhiteSpace
rawCode.add(vm.stream.readChar)
parseEnum[OpCode](rawCode)
これでOpCode
が返ってくるようになりましたので、これらを使って実行できる関数を作り、外部に公開します。
proc run*(vm: VirtualMachine) =
while not vm.stream.atEnd:
vm.exec(vm.readOpcode)
最後にファイルを受け取ってVMを呼び出す部分を書いて完成です。ここは本題から外れるため特に解説はしません。
import os
import streams
import strformat
import error
import virtual_machine
if isMainModule:
if paramCount() < 1:
error("No source file passed")
let sourceFile = commandLineParams()[0]
if not fileExists(sourceFile):
error(fmt"File '{sourceFile}' was not exists")
let vm = newVirtualMachine(newFileStream(sourceFile))
vm.run
実行してみる
nimbleでビルドして実行します。詳細はGitHubのREADME参照。
(以下の例では見やすさのためにBboLangのソースを命令毎に改行していますが動作には関係ありません)
HelloWorld
bobbBbooboooB
oobb
bobbBbboobobB
oobb
bobbBbbobbooB
oobb
bobbBbbobbooB
oobb
bobbBbbobbbbB
oobb
bobbBbobbooB
oobb
bobbBboooooB
oobb
bobbBbobobbbB
oobb
bobbBbbobbbbB
oobb
bobbBbbbooboB
oobb
bobbBbbobbooB
oobb
bobbBbboobooB
oobb
bobbBboooobB
oobb
$ bbolang HelloWorld.bbolang
Hello, World!
bobbB~~B
でスタックに文字コードをpushし、oobbで文字を出力。
四則演算
bobbBbboB
bobbBbooB
bbob
bobbBbbB
bbbb
oobo
$ bbolang Calculate.bbolang
27
6 * 4 + 3
の結果を出力。
さいごに
お疲れ様でした。ノリで作った雑で単純なものなので特に書くこともなく、コードを淡々と並べ立てただけであまり参考にはならないかもしれませんが、いかがでしたでしょうか。よければ一度お手元で動かしてみてください。できることが単純すぎるので、やる気と時間があればラベルなどの少し複雑な機能を追加することもあるかもしれません。また、これもやる気と時間があればブラウザ上で動かせるサイトを作るのもいいかなと考えています。
ではまた。
追記
この記事の執筆後、OpCodeの長さを4文字から5文字に拡張してラベルを追加したり条件分岐をできるようにしてFizzBuzzやフィボナッチ数列をBboLangで書いてみたものの、続編の記事を書く前に心が折れてしまったのでここで供養しておきます。いつか気が向いたら続編を執筆するかもしれません。
(Nimの配列をスタック・ヒープとして使ってるせいで実行速度が絶望的ですが...)
FizzBuzz
https://github.com/bbo51dog/BboLang/blob/develop/example/FizzBuzz.bbolang
boooo BoB
boobb BoB
obooo BoB # LABEL 0
boobo BoB
boooo BbbbbobooooboobooooooB # 1_000_000
obobo BbB # JUMPEQ 1
boobo BoB
boooo BbB
bbooo
boobb BoB
boobo BoB
oooob
boobo BoB
# x % 15 == 0
boobo BoB
boooo BbbbbB
bbboo
boooo BoB
obobo BbooB
# x % 5 == 0
boobo BoB
boooo BbobB
bbboo
boooo BoB
obobo BbobB
# x % 3 == 0
boobo BoB
boooo BbbB
bbboo
boooo BoB
obobo BbboB
oboob BbbB
obooo BbooB # LABEL 4 (15)
boooo BboooooB #SP
ooooo
boooo BbooobboB #F
ooooo
boooo BbboboobB #i
ooooo
boooo BbbbboboB #z
ooooo
boooo BbbbboboB #z
ooooo
boooo BbooooboB #B
ooooo
boooo BbbbobobB #u
ooooo
boooo BbbbboboB #z
ooooo
boooo BbbbboboB #z
ooooo
oboob BbbB # JUMP 3
obooo BbobB # LABEL 5 (5)
boooo BboooooB #SP
ooooo
boooo BbooooboB #B
ooooo
boooo BbbbobobB #u
ooooo
boooo BbbbboboB #z
ooooo
boooo BbbbboboB #z
ooooo
oboob BbbB # JUMP 3
obooo BbboB # LABEL 6 (3)
boooo BboooooB #SP
ooooo
boooo BbooobboB #F
ooooo
boooo BbboboobB #i
ooooo
boooo BbbbboboB #z
ooooo
boooo BbbbboboB #z
ooooo
oboob BbbB # JUMP 3
obooo BbbB # LABEL 3
boooo BboboB # LF
ooooo
oboob BoB # JUMP 0
obooo BbB # LABEL 1
フィボナッチ数列
https://github.com/bbo51dog/BboLang/blob/develop/example/Fibonacci.bbolang
boooo BboooooB # Input n=32
boobb BoB
boobo BoB
obobb BboB
oboob BbB
obooo BboB # LABEL 2
boobb BbB
# n = 1
boobo BbB
boooo BbB
obobo BbbB # JUMP 3
# n = 2
boobo BbB
boooo BboB
obobo BbbB # JUMP 3
# n>2
# n-2
boobo BbB
boobo BbB
boooo BboB
bboob
obobb BboB # CALL
boobb BboB
boobb BbB
boobo BboB
boobo BbB
# n-1
boooo BbB
bboob
obobb BboB # CALL
bbooo
obboo # RETURN
obooo BbbB # LABEL 3
boooo BbB
obboo # RETURN
obooo BbB # LABEL 1
oooob