🍊

Peggyで自作プログラミング言語を作って遊ぶ (3) 変数と予約語

2022/12/11に公開約4,600字

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

前回の記事の続きです。今回は変数を使えるようにします。

自作言語の名前

今回からは自作言語の文法が JavaScript とは異なるものになりますので自作言語に名前を付けておきます。Yuz 言語という名前にします。

今から作る文法

次のような文法を作ってみます。

let x = 5;
let y = x + 2;
x + y

let による変数宣言は ; で区切っていくつでも書くことができ、最後に出力の式を一つ書きます。出力の式の後には ; を付けません。このプログラムの出力は 12 となります。

これと同じことを行う JavaScript のコードは

(() => { const x = 5; const y = x + 2; return x + y; })()

となります。このコードに変換して eval() で実行することを目指します。

予約語

Yuz 言語では let は予約語となります。今後 if 式や真偽値も作る予定なのでとりあえず以下の 6 個を Yuz 言語の予約語とします。

  • let
  • true
  • false
  • if
  • then
  • else

予約語は変数名として使えないようにする必要があります。

予約語を考慮しない場合

まずは予約語のことを考えずに書いてみます。Peggy のコードは次のようになります。コード中の `${hoge}` は JavaScript のテンプレートリテラルという記法です。

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

Program
  = vars:(Variable _ ";" _)* e:Expression {
      const varsCode = vars.reduce((acc, x) => `${acc}${x[0]}; `, "");
      const returnCode = `return ${e};`;
      return `(() => { ${varsCode}${returnCode} })()`;
    }

Variable
  = "let" __ i:Identifier _ "=" _ e:Expression {
      return `const ${i} = ${e}`;
    }

Expression
  = head:Primary tail:(_ Operator _ Primary)* {
      return tail.reduce((acc, x) => `${acc} ${x[1]} ${x[3]}`, head);
    }

Operator
  = "+"
  / "-"

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

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

Identifier
  = [A-Za-z_] [0-9A-Za-z_]* { return text(); }

__
  = [ \t\n\r]+

_
  = [ \t\n\r]*

Identifier は変数名の規則を表しています。変数名には英字 [A-Za-z] と数字 [0-9] と下線 _ を使えるようにしました。ただし変数名の先頭には数字を置けないようにしました。

変数宣言の文法は let a = 1; のようになり、leta の間には 1 文字以上の空白文字が必要です。そこで 1 文字以上の空白文字を __ としました。

変数名に予約語を使えないようにする

変数名に予約語を使えないようにします。正規表現でいう否定先読みを用います。

記法 意味
!A B A にマッチせず B にマッチする
A !B A にマッチし、その後に続くものが B にマッチしない

これを使って Identifier の定義を次のようにすると変数名に予約語を使えなくなります。

Identifier
  = !ReservedWord IdentifierStart IdentifierContinue* {
      return text();
    }

ReservedWord
  = ("let" / "true" / "false" / "if" / "then" / "else") !IdentifierContinue

IdentifierStart
  = [A-Za-z_]

IdentifierContinue
  = [0-9A-Za-z_]

ReservedWord には !IdentifierContinue が必要です。これがないと let trueabc = 5; のときに let true を読み込んだ時点で変数名に予約語が使われたと判断してパースエラーにしてしまいます。

JavaScript の予約語を避ける

Yuz 言語の文法はこれで OK です。しかし JavaScript に変換して実行する都合でもう一つ考えないといけないことがあります。それは JavaScript の予約語を避けることです。たとえば Yuz 言語のコードで

let for = 5;

と書くと、Yuz 言語の予約語とは衝突しませんが JavaScript の予約語と衝突します。また、Arraydocument といった JavaScript の既存のオブジェクト名との衝突も避けたいです。そこで次のような簡単な対策をしておきます。

Identifier
  = !ReservedWord IdentifierStart IdentifierContinue* {
      return "$" + text();
    }

こうすると Yuz 言語の let for = 5;const $for = 5; という JavaScript コードに変換されます。JavaScript は変数名に $ を使うことができます。これで大抵の衝突は避けられるでしょう。

実行結果を出力する

まとめると Peggy のコードは次のようになります。

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

Program
  = vars:(Variable _ ";" _)* e:Expression {
      const varsCode = vars.reduce((acc, x) => `${acc}${x[0]}; `, "");
      const returnCode = `return ${e};`;
      return `(() => { ${varsCode}${returnCode} })()`;
    }

Variable
  = "let" __ i:Identifier _ "=" _ e:Expression {
      return `const ${i} = ${e}`;
    }

Expression
  = head:Primary tail:(_ Operator _ Primary)* {
      return tail.reduce((acc, x) => `${acc} ${x[1]} ${x[3]}`, head);
    }

Operator
  = "+"
  / "-"

Primary
  = Float
  / Identifier

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

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

Identifier
  = !ReservedWord IdentifierStart IdentifierContinue* {
      return `$${text()}`; //"$" + text()
    }

ReservedWord
  = ("let" / "true" / "false" / "if" / "then" / "else") !IdentifierContinue

IdentifierStart
  = [A-Za-z_]

IdentifierContinue
  = [0-9A-Za-z_]

__
  = [ \t\n\r]+

_
  = [ \t\n\r]*

Yuz 言語で

let x = 5;
let y = x + 2;
x + y

とすると

(() => { const $x = 5; const $y = $x + 2; return $x + $y; })()

という JavaScript コードに変換されます。これを実行すると 12 を得ます。

Input: let x = 5; let y = x + 2; x + y, Output: 12

ただし同名の変数を二度宣言したり宣言していない変数を使ったりすると変換後の JavaScript コードを eval() で実行したときに実行時エラーが起こります。

Input: let x = 5; let y = x + 2; hoge + y, Error: $hoge is not defined

まとめ

今回は変数を使えるようにしました。次回は掛け算、割り算、大小比較などの二項演算子 12 種類と if 式を使えるようにします。また、計算の優先順位を表す () を使えるようにします。

Discussion

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