🧩

Peggyで自作言語を作って遊ぶ (2) 二項演算子と空白文字と浮動小数点数

2022/12/10に公開約4,100字

自作プログラミング言語をなるべく手軽かつ簡単に作る方法を紹介します。また、実際に自作プログラミング言語を作ってみます。

前回の記事の続きです。今回は自作言語に二項演算子の連鎖と空白文字の無視と浮動小数点数の文法を付け加えてみます。

二項演算子の連鎖

前回の記事で足し算と引き算ができる言語を作りましたが、7+2398-4 のように二項演算子を一度しか使えませんでした。そこで 7+23-5+10 のように二項演算子を複数回使えるようにします。

Program
  = head:Integer tail:(Operator Integer)+ {
      return tail.reduce((acc, x) => acc + " " + x[0] + " " + x[1], head);
    }

Operator
  = "+"
  / "-"

Integer
  = nums:[0-9]+ {
      return nums.reduce((acc, x) => acc + x, "");
    }

Program の定義を Integer (Operator Integer)+ にしました。これは Integer の次に 1 個以上の Operator Integer が続くということです。たとえば 3 個続くのであれば Integer Operator Integer Operator Integer Operator Integer となり、7+23-5+10 のようなコードにマッチします。

Program の定義の {} 内では tail に対して reduce() を行っています。tail には N 個の Operator Integer が入っています。それらを x という名前で 1 個ずつ取り出しています。各 x について、x[0] には Operatorが、x[1] には Integer が入っています。

これで 7+23-5+10 のような複数個の二項演算子が続くコードに対応できるようになりました。

Input: 7+23-5+10, Output: '7 + 23 - 5 + 10'

また、Program の定義を

Program
  = head:Integer tail:(Operator Integer)*

に変えてみます。(Operator Integer)+(Operator Integer)* に変えました。そうすると 1 個以上ではなく 0 個以上の Operator Integer になります。これで二項演算子が 0 個のコードにも対応できるようになりました。

Input: 23, Output: '23'

空白文字を無視する

プログラミング言語の多くは空白文字を無視します。たとえば 2+32 + 3 は同じコードと見なします。そこで半角スペースとタブと改行を空白文字として無視するようにします。

Program
  = _ head:Integer tail:(_ Operator _ Integer)* _ {
      return tail.reduce((acc, x) => acc + " " + x[1] + " " + x[3], head);
    }

Operator
  = "+"
  / "-"

Integer
  = nums:[0-9]+ {
      return nums.reduce((acc, x) => acc + x, "");
    }

_
  = [ \t\n\r]*

上記のコードで定義した _ は 0 文字以上の空白文字です。それを Program の定義の中のあらゆる隙間に入れました。Program{} 内の tail.reduce() では x[0]x[2] が空白文字で、x[1]Operator で、x[3]Integer となります。そのため、変換後の JavaScript コードに x[1]x[3] だけを入れるようにしました。こうすることで自作言語での空白文字の入力を許しつつ JavaScript へ変換するときには無視することができます。

実際に試してみると自作言語のコード内の空白文字が無視されることがわかります。

Input: 1   + 11  -1 +111 -1+11-  1, Output: '1 + 11 - 1 + 111 - 1 + 11 - 1'

整数の定義を修正する

現在は Integer = [0-9]+ としています。これだと 07 のような先頭が 0 の数字も許されます。そこで次のように修正します。

Integer
  = head:[1-9] tail:[0-9]* { return tail.reduce((acc, x) => acc + x, head); }
  / "0"

先頭が 0 の数字は [1-9] [0-9]* の定義にマッチしないためパースエラーとなります。しかしそれだと単体の 0 もパースエラーになってしまうので "0" の定義を付け足しました。

Peggy の省略記法を使う

reduce() で書くのがつらくなってきたので Peggy の省略記法を使うことにします。上記の Integer の定義は次のように書き直すことができます。

Integer
  = [1-9] [0-9]* { return text(); }
  / "0"

省略記法は { return text(); } のところです。定義にマッチしたものをすべて連結して一つの文字列にしてくれます。これを使えば reduce() による連結よりも表記が簡単になります。

浮動小数点数を使えるようにする

整数に加えて、浮動小数点数も使えるようにします。

Program
  = _ head:Float tail:(_ Operator _ Float)* _ {
      return tail.reduce((acc, x) => acc + " " + x[1] + " " + x[3], head);
    }

Operator
  = "+"
  / "-"

Float
  = Integer ("." [0-9]+)? { return text(); }

Integer
  = [1-9] [0-9]* { return text(); }
  / "0"

_
  = [ \t\n\r]*

A? は 0 個もしくは 1 個の A という意味です。よってこの Float の定義は Integer も含むようになっています。そして Program の定義を Float (_ Operator _ Float)* にしました。これで 1 + 2.34 のように整数と浮動小数点数が混ざったコードを受け入れられるようになりました。

Input: 1 + 2.34, Output: '1 + 2.34'

計算結果を出力する

Program の一つ上に Start を作り、変換後の JavaScript コードを eval() で実行するようにします。そうすると自作言語の実行結果を出力することができます。

Start
  = _ p:Program _ { return eval(p); }

Program
  = head:Float tail:(_ Operator _ Float)* {
      return tail.reduce((acc, x) => acc + " " + x[1] + " " + x[3], head);
    }

Input: 1.23 +7.89 +2-  3.52, Output: 7.6

まとめ

今回は二項演算子の連鎖と空白文字の無視と浮動小数点数の受け入れができるようになりました。次回は変数を使えるようにします。今回までは自作言語の文法が JavaScript とほぼ同じでしたが、次回は JavaScript と異なる文法にしてみます。

Discussion

ログインするとコメントできます