この記事を書くときに使用した環境は以下の通りです。
CPU Celeron N3160 Quad core(1.60Ghz)
RAM 4GM
OS Windows 10 Home(1019 18363.535)
IDE VSCode + Tiny C Compiler
使用ソフトウェアのリンク集です
VSCode(https://code.visualstudio.com/)
Tiny C Compiler(https://bellard.org/tcc/)
#対象者
- C言語の知識がある程度ある
- 言語の作り方があまりわからない
#本題
今回は、C言語による、簡単な言語の実装方法について説明していこうと思います
この言語はネイティブexeを出力するわけではありません
exeの第一引数にソースコードへのパス
第一引数がなければ、インタプリタモードという動作をとるプログラムを作成していきます
#だいたいの仕組み
- main
- argcを見て第一引数が存在するかを確認する
- メモリ空間の初期化
- 第一引数があるなら、そのソースコードを実行して終了
- 第一引数がないなら、インタプリタを起動して終了
- 使用したメモリの開放
- 終了
#自作言語の仕様
i32は32ビットとする(そもそも変数型名に32ってある)
整数リテラルの基本的な型は16進数とする
メモリにつけられる名前は、基本的に3文字とする
メモリは、36^3 (a~zとA~Zと0~9が3文字)を名前とする
コメントは$から行末までとする
#こんな感じ
mem def __a i32 $__aという名前でint型の変数を定義
mem def __b i32 $__bという名前でint型の変数を定義
mem set __a 01d $__aに001をセット
mem set __b 01h $__bに001をセット
mem def __c int $__cという名前でint型の変数を定義
add __a __b __c $__aと__bとの和を__cに代入する(__c=__a+__bというような感じ)
#この言語について
- 実装がしやすい
- 感のいい人は気づいたかもしれないが、すべて3文字で構成されている
- コメントが$から行末なので、gets関数を呼び出して文字例を廃棄すればいい
- 3文字なので最初から、メモリを用意することができる。しかし型という概念があるため、最初はvoidポインタ型のNULLにする必要がある
- 見てわかる通り文法がとてもシンプル、なのだが可読性は低い
- アセンブラに似ている
- アセンブラの足し算は、add eax,ebxのような感じで足し算も命令
- これは、アセンブラでもC言語でもいえることであるが、毎回変数を定義する(アセンブラだと、スタックをespというレジスタの値を操作して、確保している)
- 読みにくい
- 先述したとうり文法をすごくシンプルにしたせいで、逆に読みにくくなった
- 書きにくい
- これは、簡単なことで、変数がいつ定義されるのかがわかりにくい
実装方法
run
| ファイルを開く
| グローバル変数の仮想メモリをNULLで初期化する
| char[3]型のa,b,c,dを定義する
| ループに入る
| | 三文字読み込みそれをaとする
| | 三文字読み込みそれをbとする
| | 三文字読み込みそれをcとする
| | 三文字読み込みそれをdとする
| | 文法チェック、ダメならエラーを表示して終了
| | aとbを参考にして、用途に合っている関数をcとdを引数として呼ぶ
| | 繰り返し
| +--
+--
#実装
とりあえず、run関数を作成させる
先ほど書いたフロートチャートをもとに実装すると次のようになる
void run(){
FILE *fp=fopen("src.txt","r");
char line[512];
char a[4]={0};
char b[4]={0};
char c[4]={0};
char d[4]={0};
while(fgets(line,512,fp)!=NULL)
{
a[0]=line[0];
a[1]=line[1];
a[2]=line[2];
b[0]=line[4];
b[1]=line[5];
b[2]=line[6];
c[0]=line[8];
c[1]=line[9];
c[2]=line[10];
d[0]=line[12];
d[1]=line[13];
d[2]=line[14];
printf("%s %s %s %s\n",a,b,c,d);
}
}
>tcc -run main.c
mem def __a i32
mem def __b i32
mem set __a 01d
mem set __b 01h
mem def __c int
add __a __b __c
>
コメントは削除されましたね
次は、printfを呼び出しに書き換えたもの(一部)を書きます
#呼び出し
if (!strcmp(a, "mem"))
{
if (!strcmp(b, "def"))
mem_def(c, d);
if (!strcmp(b, "set"))
mem_set(c, d);
}
if (!strcmp(a, "add"))
{
mem_add(b, c, d);
}
mem_defやmem_set、mem_addの実装は今は省きます(下の方にあると思う)
> tcc -run main.c
mem def __a i32 0 0 1
mem def __b i32 0 0 2
mem set __a 001 0 0 1
mem set __b 001 0 0 2
mem def __c i32 0 0 3
add __a __b __c
Cleanuping Memorys
free [0,0,1](1)
free [0,0,2](1)
free [0,0,3](2)
##この結果の見み方
Cleanuping Memorys
の前はすべて命令文と、命令文で出力された文字例
Cleanuping Memorys
は、mem defで確保したメモリの自動開放です
Cleanuping Memorys
の後は、メモリ開放ログです、free [a,b,c](v)
aはメモリの第一要素番号
bはメモリの第二要素番号
cはメモリの第三要素番号
vは値
#ソースコード全体
※1ファイルにまとめて書いたので汚いです
※コメント処理が適当です
/**
* @file main.c
* @brief SimpleLang Interpreter(SL)
* @author Syoch
* @date 2020/10/9(JPN)
*/
/*Include*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* Macro definetion*/
#define SIZE(x) ((sizeof(x)) / (sizeof(x[0])))
#define char2int_a(c, p) \
int p##A = char2int(c[0]); \
int p##B = char2int(c[1]); \
int p##C = char2int(c[2]);
/**
* @def
* i32 -> (long int)
* 32bit integerter
*/
#define i32 long int
/**
* @def
* MEMTYPE_I32 ->1
* memory type int 32
*/
#define MEMTYPE_I32 ((char)1)
//! memorys (void pointer three array)
void *mems[59][59][59] = {NULL};
//! memorys use table (char three array)
char usemems[59][59][59] = {0};
//! memorys type table (char thee array)
char typmems[59][59][59] = {0};
//! Convert table using by char2int
char convtable[] = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* @fn
* _ a-z A-Z 0-9 To 0-58 Converting function
* @brief Converter
* @param c Convert from
* @return 0-58 value
*/
int char2int(char c)
{
int l, i;
for (l = 0; l < SIZE(convtable); l++)
{
if (convtable[l] == c)
{
i = l;
break;
}
}
return l;
}
void error() {}
void mem_def(char c[3], char d[3])
{
printf("%d %d %d\n", char2int(c[0]), char2int(c[1]), char2int(c[2]));
char2int_a(c, mem);
if (!strcmp(d, "i32"))
{
typmems[memA][memB][memC] = MEMTYPE_I32;
usemems[memA][memB][memC] = 1;
mems[memA][memB][memC] = (i32 *)malloc(sizeof(i32) * 1);
}
else
{
printf("Unknown Memory type\n");
error();
}
}
void mem_set(char c[3], char d[3])
{
printf("%d %d %d\n", char2int(c[0]), char2int(c[1]), char2int(c[2]));
char2int_a(c, mem);
if (typmems[memA][memB][memC] == MEMTYPE_I32)
{
*((i32 *)mems[memA][memB][memC]) = (i32)atoi(d);
}
}
void mem_add(char b[3], char c[3], char d[3])
{
char2int_a(b, memB);
char2int_a(c, memC);
char2int_a(d, memD);
if (typmems[memBA][memBB][memBC] == MEMTYPE_I32 &&
typmems[memCA][memCB][memCC] == MEMTYPE_I32 &&
typmems[memDA][memDB][memDC] == MEMTYPE_I32)
{
(i32) * mems[memDA][memDB][memDC] =
(i32)*mems[memBA][memBB][memBC] +
(i32)*mems[memCA][memCB][memCC];
}
else
{
printf("Invalid Memory type\n");
error();
}
}
void run()
{
FILE *fp = fopen("src.txt", "r");
char line[512];
char a[4] = {0};
char b[4] = {0};
char c[4] = {0};
char d[4] = {0};
while (fgets(line, 512, fp) != NULL)
{
a[0] = line[0];
a[1] = line[1];
a[2] = line[2];
b[0] = line[4];
b[1] = line[5];
b[2] = line[6];
c[0] = line[8];
c[1] = line[9];
c[2] = line[10];
d[0] = line[12];
d[1] = line[13];
d[2] = line[14];
printf("%s %s %s %s ", a, b, c, d);
if (!strcmp(a, "mem"))
{
if (!strcmp(b, "def"))
mem_def(c, d);
if (!strcmp(b, "set"))
mem_set(c, d);
}
if (!strcmp(a, "add"))
{
mem_add(b, c, d);
}
}
}
/**
* @fn
* @brief Memory Free
* @param None
* @return None
*/
void free_mems()
{
printf("\nCleanuping Memorys\n");
int i, j, k;
for (i = 0; i < SIZE(mems); i++)
{
for (j = 0; j < SIZE(mems); j++)
{
for (k = 0; k < SIZE(mems); k++)
{
if (usemems[i][j][k] == 1)
{
printf("free [%d,%d,%d](%d)\n", i, j, k, *((i32 *)mems[i][j][k]));
free(mems[i][j][k]);
}
}
}
}
}
/**
* @fn
* Program entry pointer function
* @brief Entrypoint
* @param argc Program Argument Length
* @param argv Program Argument array(String)
* @return int(0)
*/
int main(int argc, char const *argv[])
{
run();
free_mems();
return 0;
}
コメントは、適当です。
今回は、パーサも自作できましたがyacc?などを使うともっともっと楽に言語の作成できます
しかし僕は、ちょっとよくわからなかったのでC言語で書きました()
それにこれは出力も入力もできない言語ですが各自で、機能を追加していけばいい言語ができると思います
スペースで区切ったりするようにしてもいいかもしれませんね。
今更気づいたのですが、インタプリタモードを実装していませんでした
2020/01/16(JPN)追記:現在、コンパイラを書いています、完成したら記事にしようと思いますので楽しみにしていてください()