2
5

Unityでのゲーム作り日記#3 ~自作言語の字句解析器を作る~

Last updated at Posted at 2024-01-09

前回の記事はこちら

今回は、自作言語であるターコイズ言語の字句解析器を完成させていきたいと思います。
プログラミング言語を作りたいと思っている人の助けにもなれたら幸いです。

ターコイズ言語の概要

ターコイズ言語は手続き言語として作ります。
文法は、JSライク?にしていきます。
変数宣言は
var VARIABLENAME = 3
関数宣言は
func FUNCTIONNAME(ARGMENT1, ARGMENT2, ...){PROCESS}
関数呼び出しは
FUNCTIONNAME(ARGMENT1, ARGMENT2, ...)
でできるようにしたいと思います。

プログラミング言語の作り方

ターコイズ言語_01.png
このように、字句解析器、構文解析器、インタープリタを作ればよいです。

実際に作っていく

字句解析器

字句解析とは、例えば var x = 3 というソースコードを分解して、var, x, =, 3 というトークンに変換することです。
そして、その字句解析をするのが字句解析器です。

まず、字句解析器を書く前に字句解析器で生成するトークンのクラスを作っていきます。
それがこちらです。

Tokens.cs
namespace Tokens{
   public interface Token{ //インターフェイス
       object getValue(); //値を返す関数
       int getLineno(); //行番号を返す関数
   }

   public class Number : Token{ //数字
       private int lineno; //行番号
       private double _num; //値

       public Number(int lineno, double num){
           this.lineno = lineno;
           _num = num;
       }

       public object getValue(){
           return _num;
       }

       public int getLineno(){
           return lineno;
       }
   }

   public class Identifier : Token{ //識別子(変数名、関数名など)
       private int lineno; //行番号
       private string _name; //名前

       public Identifier(int lineno, string name){
           this.lineno = lineno;
           _name = name;
       }

       public object getValue(){
           return _name;
       }

       public int getLineno(){
           return lineno;
       }
   }

   public class Operator : Token{ //演算子
       private int lineno; //行番号
       private string _op; //記号

       public Operator(int lineno, string op){
           this.lineno = lineno;
           _op = op;
       }

       public object getValue(){
           return _op;
       }

       public int getLineno(){
           return lineno;
       }
   }

   public class EOFToken : Token{ //プログラムの終わりを表すトークン
       private int lineno; //行番号

       public EOFToken(int lineno){
           this.lineno = lineno;
       }

       public object getValue(){
           return "EOF";
       }

       public int getLineno(){
           return lineno;
       }
   }
}

そして、これが私が書いた字句解析器です。

Lexer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Tokens; //さっきのトークンの名前空間

public class Lexer{
   private System.IO.StringReader code;
   private List<Token> queue = new List<Token>(); //トークンの配列
   private int lineno = 1; //今読み込んでいる場所
   private bool hasMore = true;
   private string operators = ""; //operators

   public Lexer(string code_str, string operators){
       this.code = new System.IO.StringReader(code_str); //ソースコードを読み取る

       this.operators = operators; //使う演算子を読み込み
   }

   private bool fillQueue(int i){ //i文字分読み込む(あまり気にしなくて大丈夫です)
       while(i >= queue.Count)
           if(hasMore){
               readLine();
           }else
               return false;
       return true;
   }

   private void readLine(){ //queueに1行分読み込む
       string line;
       if(code.Peek() <= -1){
           hasMore = false;
           queue.Add(new EOFToken(lineno));
           return;
       }
       line = code.ReadLine();

       int endPos = line.Length;
       int i = 0;
       string str = "";

       while(i < endPos){ //行が終わるまで繰り返す
           str = "";
           if(line[i] == ' ' || line[i] == '\t' || line[i] == '\n' || line[i] == '\r'){ //space, tab, indention
               i++; //文字がタブかスペースか改行だったら1文字分飛ばす
           }else if(line[i] == '#'){ //comment
               return; //文字がコメント文字だったらリターンする。
           }else if(line[i] >= '0' && line[i] <= '9'){ //number
               while(i < endPos && line[i] >= '0' && line[i] <= '9'){
                   str += line[i++];
               }
               if(i < endPos && line[i] == '.'){
                   str += line[i++];
               }
               while(i < endPos && line[i] >= '0' && line[i] <= '9'){
                   str += line[i++];
               }

               queue.Add(new Number(lineno, double.Parse(str))); //数字をNumberトークンとしてqueueに追加する
           }else if(line[i] >= 'a' && line[i] <= 'z' || line[i] >= 'A' && line[i] <= 'Z' || line[i] == '_'){ //identifier
               str += line[i++];
               while(i < endPos && (line[i] >= 'a' && line[i] <= 'z' || line[i] >= 'A' && line[i] <= 'Z' || line[i] == '_' || line[i] >= '0' && line[i] <= '9')){
                   str += line[i++];
               }

               queue.Add(new Identifier(lineno, str)); //識別子をIdentifierトークンとしてqueueに追加する
           }else if(operators.Contains(line[i])){ //operator
               str += line[i++];
               while(i < endPos && operators.Contains(str + line[i])){
                   str += line[i++];
               }

               queue.Add(new Operator(lineno, str)); //演算子をOperatorトークンとしてqueueに追加する
           }
       }

       lineno++;
   }

   public Token read(){ //一文字分読み込み、カウントを進める
       if(fillQueue(0)){
           if(queue.Count > 0){
               Token r = queue[0];
               queue.RemoveAt(0);
               return r;
           }else
               return new EOFToken(lineno);
       }else
           return new EOFToken(lineno);
   }

   public Token peek(int i){ //i文字分読み込み、i文字目を返す
       if(fillQueue(i)){
           if(queue.Count > 0)
               return queue[i];
           else
               return new EOFToken(lineno);
       }else
           return new EOFToken(lineno);
   }
}

例えば、次のように使います。

Lexer lexer = new Lexer("3 + 5 * x", "+\0*"); //引数operatorsの文字列は使う演算子をnull文字で区切ります
lexer.read(); //return : Number(3)
lexer.read(); //return : Operator(+)
lexer.peek(1); //return : Operator(*)
lexer.read(); //return : Number(5)
lexer.peek(0); //return : Operator(*)
lexer.read(); //return : Identifier(x)
lexer.read(); //return : EOFToken(lineno)

次回は構文解析器を作っていきます。
次回の記事はこちら

2
5
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
2
5