【入門】プログラミング言語を自作する方法①|まずは簡単なインタプリタを作る

自作プログラミング言語 C言語

はじめに

この記事では、簡単なプログラミング言語(電卓レベル)を自作しながら、
言語の仕組み(字句解析・構文解析)を理解します。

プログラミングを作成するにあたって、参考にした実行エンジンですが、もちろんZend Engineです。

さて早速作り方というか、どのようにして製造を進めているかというと、

基本的にはコンパイルパイプラインに則って進めてます。

1. ソースコード

自作の言語です。(テキスト形式)

2. 字句解析(Lexical Analysis / Scanning)

ソースコードを「意味のある最小単位(トークン)」にバラバラにします。

3. 構文解析(Syntax Analysis / Parsing)

トークンの並びが、言語のルール(文法)に合っているかチェックし、「抽象構文木(AST)」という木構造を作ります。

4. 意味解析(Semantic Analysis)

文法は合っているけど、中身がおかしくないか?を調べます。

5. 中間コード生成・最適化(Intermediate Code Generation & Optimization)

木構造を、実行しやすい形(中間表現)に変換し、無駄な計算を削ります。

6. コード生成(Code Generation)

最終的なターゲットを書き出します。

ソースコードが実行されるまでの間にこれら5つの工程があります。

コード
 ↓
字句解析
 ↓
構文解析
 ↓
実行

(今回が初めての言語作成ですので、まだまだ曖昧なところが多々あります。あくまでも参考程度でお願いいたします。。。)

新しく作る言語の名前・由来

作成する言語の名前ですが、もう決めてます!

今回は言語の名前が、goemon で、実行エンジンの名前が Kama です。

(モチーフについては皆さんお気づきかと思いますが、石川五右衛門です。響きがかっこいいなと思い命名しました笑)

ソースコード

↓リポジトリになります。

GitHub - yu-corder/goemon-src
Contribute to yu-corder/goemon-src development by creating an account on GitHub.

ディレクトリ構成ですが、こんな感じです。

Kama ディレクトリが実行エンジンのディレクトリになります。

examples ディレクトリが実際のgoemon のソースになります。

push 1
push 1
add
dup
print
push 1
jz 4
jmp 4
//これが今のgoemonソースになります。
//コンパイラがpushやaddなどの文字列を探します。

はい、これが今のgoemonプログラムになります。(しょぼいですよね。)

このソースをOpcodeにコンパイルします。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum { 
    OP_PUSH,
    OP_ADD,
    OP_SUB,
    OP_MUL,
    OP_DUP,
    OP_SWAP,
    OP_POP,
    OP_MOD,
    OP_EQ,
    OP_JMP,
    OP_JZ,
    OP_PRINT,
    OP_HALT 
} OpCode;

C言語で作成した、コンパイラで、goemon ファイルを一行ずつ読み取り、そして、Opcodeに変換し、バイナリファイルに保存します。

    FILE *src = fopen("examples/study.goe", "r");
    if (!src) { perror("Goemon's scroll (study.goe) not found"); return 1; }

    int bytecode[1024];
    int count = 0;
    char line[256];

    while (fgets(line, sizeof(line), src)) {
        if (strncmp(line, "push", 4) == 0) {
            int val = atoi(line + 5);
            bytecode[count++] = OP_PUSH;
            bytecode[count++] = val;
        } else if (strstr(line, "add")) {
            bytecode[count++] = OP_ADD;
//ここのwhile文でpushやaddなどの文字列を探してOP_PUSHやOP_ADDなどのOpCodeに変換します。実行VMが実行できる形です。実行VM側でも同じ構造体を定義しています。
....省略(クソコードすぎるので)

バイナリファイルを最終的に実行エンジンが読み取り実行していくといった形になります。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef enum {
    OP_PUSH,
    OP_ADD,
    OP_SUB,
    OP_MUL,
    OP_DUP,
    OP_SWAP,
    OP_POP,
    OP_MOD,
    OP_EQ,
    OP_JMP,
    OP_JZ,
    OP_PRINT,
    OP_HALT
} OpCode;

void run(int* program) {
    int stack[256];
    int sp = -1;
    int pc = 0;

    while (true) {
        int instruction = program[pc++];

        switch (instruction) {
            case OP_PUSH:
                stack[++sp] = program[pc++];
                break;
            case OP_ADD: {
                int b = stack[sp--];
                int a = stack[sp--];
                stack[++sp] = a + b;
                break;
            }
            case OP_SUB: {
                int b = stack[sp--];
                int a = stack[sp--];
                stack[++sp] = a - b;
                break;
            }
//ここのwhile文でバイナリファイルを読み取って、OpCodeがないか探します。見つかった場合、それぞれの処理を実行します。
...省略(クソコードすぎるので)

で、最終的にはこんな感じで実行結果が返ってきます。

test.com@sample goemon-src % make run
./kama-c
絶景かな! Compiled study.goe to study.gb
./kama-e examples/study.gb
VM Output: 2

え、これがプログラミング言語なの?と思ったかたいらっしゃると思いますが、ごもっともです。

私もしょぼいと思ってます。ただ、今回は作ってみた①ということで今後に期待してください!まだまだ、作成途中ですので!

道は果てしない、、、

最後に

毎回恒例になっていますが、私はまだ、未熟者ゆえに間違った知識や、コードをお見せることもありますので、どうか参考程度にお願いいたします。

↓part2になります。参考になればと思います。

コメント

タイトルとURLをコピーしました