6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

自作Esolangのようなもの"BboLang"を作った話

Last updated at Posted at 2021-06-27

はじめに

久しぶりの記事投稿、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を作っていきましょう。とりあえず情報を保持するためにオブジェクトを。

virtual_machine.nim
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を積むだけです)。このままではスタックに何もできないので操作を追加してみましょう。

virtual_machine.nim
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通りのみです。まずはその命令たちを落とし込みましょう。

virtual_machine.nim
type
  OpCode {.pure.} = enum
    Add
    Sub
    Mul
    Div
    Push
    Pop
    EchoChar
    EchoInt

列挙型で全ての命令のオペコードを並べました。これだけでは意味がないのでオペコードを受け取って処理をする関数を作ります。

virtual_machine.nim
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から読み取る部分を先に書きます。が、予期しない入力があった時のためにエラー出力用の関数も作っておきます。

error.nim
import terminal

proc error*(message: string) =
  stdout.styledWrite(fgRed, "Error: ", resetStyle)
  stdout.writeLine(message)
  quit 1

これでいい感じに色をつけてエラーを出力し、終了してくれるようになりました。
何故stderrではなくstdoutにしたのかは自分でも謎です。いつか気が向けば直します。
ともかく、これでエラーには対応できるようになったので今度こそ数値を読み取る部分を書きましょう。

virtual_machine.nim
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()関数を完成させましょう。

virtual_machine.nim
proc exec(vm: VirtualMachine, op: OpCode) =
  case op
  #略
  of OpCode.Push:
    vm.stack.push(vm.readNum)
  #略

コードの実行部分は完成です。
続いて数値以外の部分も読み取れるようにしていきます。
まず、BboLangでは全ての空白文字を無視するので、空白文字を全てスキップできるようにします。
(ここまで書いて気付いたのですが、リテラル内の空白文字は無視できていませんでしたね。気付かなかったことにしておきます。)

virtual_machine.nim
proc skipWhiteSpace(stream: Stream) =
  while stream.peekStr(1).isEmptyOrWhitespace:
    discard stream.readChar

はい、空白文字を飛ばせるようになりました。
続いてオペコードの名前と実際のオペコードを結びつけたいので、enumに手を加えます。

virtual_machine.nim
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文字は重複を避けるただの識別子のようなものです。
では本題に戻ってオペコードを読み取りましょう。

virtual_machine.nim
proc readOpcode(vm: VirtualMachine): OpCode =
  var rawCode = ""
  for i in 1..4:
    vm.stream.skipWhiteSpace
    rawCode.add(vm.stream.readChar)
  parseEnum[OpCode](rawCode)

これでOpCodeが返ってくるようになりましたので、これらを使って実行できる関数を作り、外部に公開します。

virtual_machine.nim
proc run*(vm: VirtualMachine) =
  while not vm.stream.atEnd:
    vm.exec(vm.readOpcode)

最後にファイルを受け取ってVMを呼び出す部分を書いて完成です。ここは本題から外れるため特に解説はしません。

bbolang.nim
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

HelloWorld.bbolang
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で文字を出力。

四則演算

Calculate.bbolang
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

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

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
6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?