🐚

自作プログラミング言語「shelly」を生み出す

に公開

はじめに

こんにちは。shellfishです。本日は自作プログラミング言語を製作していきたいと思う。
これらを通して普段使用している技術の成り立ちや深いところまで知ることができるのではないかと思っている。
「shelly」という名前は、私の名前から取っている。
ChatGPT先生とお話して開発に取り組みました。間違っている部分などあればご指摘いただけると幸いだ。

想定読者

  • 自作のプログラミング言語を作ってみたいがどうしたらよいのかわからない方
  • プログラミング言語の仕組みを知りたい方

基本構成

shelly/
├── src/
│   ├── lexer.c       # 字句解析
│   ├── lexer.h
│   ├── parser.c      # 構文解析
│   ├── parser.h
│   ├── ast.c         # 抽象構文木(AST)の管理
│   ├── ast.h
│   ├── interpreter.c # 実行器
│   ├── interpreter.h
│   ├── main.c        # エントリーポイント
│   ├── token.h
│   ├── utils.h
│   └── utils.c
├── include/          # ヘッダファイルを整理(大規模化を想定)
└── examples/
    └── hello.shl      # サンプルコード

いきなりこれを見ても分かりづらいので解説してもらった。

プロジェクトの全体構成について

shelly/
├── src/        # 実装ファイル(Cコード本体)
├── include/    # ヘッダファイル
└── examples/   # 実行テスト用の自作言語スクリプト

src/ 各機能ごとに実装ファイル(.c)と対応するヘッダファイル(.h)を分ける
include/ 大規模化した時にsrcから#includeできるようにヘッダを整理(今は使わない)
examples/ ユーザーが書く自作言語のソース

各ファイルの役割

ファイル 役割
main.c インタプリタのエントリーポイント。コマンドライン引数を受け取り、ソースファイルを読み込み、字句解析→構文解析→AST→実行の流れを制御。
token.h lexer と parser が「これがどんな種類のトークンか」を共有するための設計図
lexer.c / lexer.h 字句解析器 (Lexer)。文字列ソースを読み込み、トークン列に分解。print, 数字, 演算子などをトークン化。
parser.c / parser.h 構文解析器 (Parser)。トークン列を解析し、式や文の構造を表す AST を生成。例:1+2 → BinOpNode(+)
ast.c / ast.h 抽象構文木 (AST) 管理。ASTノードの生成・解放や構造体定義をまとめる。ノード種類:数字、演算、print 文など。
interpreter.c / interpreter.h 実行器 (Interpreter)。ASTを再帰的に評価して計算や出力を行う。eval(ASTNode*) の実装。
utils.c / utils.h 補助関数。ファイル読み込み、文字列操作、メモリ管理など共通処理をまとめる。

各モジュールの設計

ここまでファイル構成とその仕組みについて書いたので、そろそろ手を動かしていこう。
まずはtoken.hから始めよう。

token.h
#ifndef TOKEN_H
#define TOKEN_H

typedef enum {
    TOKEN_EOF,
    TOKEN_PRINT,
    TOKEN_NUMBER,
    TOKEN_STRING,
    TOKEN_PLUS,
    TOKEN_MINUS,
    TOKEN_MUL,
    TOKEN_DIV,
    TOKEN_LPAREN,
    TOKEN_RPAREN,
    TOKEN_UNKNOWN
} TokenType;

typedef struct {
    TokenType type;
    char* text;
} Token;

#endif
lexer.c
#include "lexer.h"
#include "token.h"
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

Token* next_token(const char** src) {
    while (isspace(**src)) (*src)++;

    if (**src == '\0') {
        Token* t = malloc(sizeof(Token));
        t->type = TOKEN_EOF;
        t->text = NULL;
        return t;
    }

    if (isdigit(**src)) {
        const char* start = *src;
        while (isdigit(**src)) (*src)++;
        int len = *src - start;
        char* buf = strndup(start, len);
        Token* t = malloc(sizeof(Token));
        t->type = TOKEN_NUMBER;
        t->text = buf;
        return t;
    }

    if (strncmp(*src, "print", 5) == 0) {
        *src += 5;
        Token* t = malloc(sizeof(Token));
        t->type = TOKEN_PRINT;
        t->text = strdup("print");
        return t;
    }

    if (**src == '"') {
        (*src)++;
        const char* start = *src;
        while (**src && **src != '"') (*src)++;
        int len = *src - start;
        char* buf = strndup(start, len);
        if(**src == '"') (*src)++;
        Token* t = malloc(sizeof(Token));
        t->type = TOKEN_STRING;
        t->text = buf;
        return t;
    }

    char c = *(*src)++;
    Token* t = malloc(sizeof(Token));
    switch (c) {
        case '+': t->type = TOKEN_PLUS; break;
        case '-': t->type = TOKEN_MINUS; break;
        case '*': t->type = TOKEN_MUL; break;
        case '/': t->type = TOKEN_DIV; break;
        case '(': t->type = TOKEN_LPAREN; break;
        case ')': t->type = TOKEN_RPAREN; break;
        default: t->type = TOKEN_UNKNOWN; break;
    }
    t->text = strndup(&c, 1);
    return t;
}
lexer.h
#ifndef LEXER_H
#define LEXER_H

#include "token.h"

Token* next_token(const char** src);

#endif
ast.h
typedef enum {
    AST_PRINT,
    AST_NUMBER,
    AST_STRING,
    AST_BINOP
} ASTNodeType;

typedef struct ASTNode {
    ASTNodeType type;
    union {
        struct { struct ASTNode* expr; } printStmt;
        struct { int value; } number;
        struct { char* value; } string;
        struct {
            struct ASTNode* left;
            struct ASTNode* right;
            char op;
        } binop;
    };
} ASTNode;
interpreter.c
#include "ast.h"
#include <stdio.h>

int eval(ASTNode* node) {
    switch(node->type) {
        case AST_NUMBER:
            return node->number.value;
        case AST_BINOP: {
            int l = eval(node->binop.left);
            int r = eval(node->binop.right);
            switch (node->binop.op) {
                case '+': return l + r;
                case '-': return l - r;
                case '*': return l * r;
                case '/': return l / r;
            }
        }
        default:
            return 0;
    }
}

void exec(ASTNode* node) {
    if (node->type == AST_PRINT) {
        ASTNode* expr = node->printStmt.expr;
        if (expr->type == AST_STRING) {
            printf("%s\n", expr->string.value);
        } else {
            printf("%d\n", eval(expr));
        }
    }
}

eval関数で数値や計算式を評価する。
exec関数で命令を実行する。

main.c
#include <stdio.h>
#include <stdlib.h>
#include "lexer.h"
#include "parser.h"
#include "interpreter.h"
#include "token.h"

// ファイルを丸ごと読み込む関数
char* read_file(const char* filename) {
    FILE* fp = fopen(filename, "rb");
    if (!fp) {
        perror("fopen");
        exit(1);
    }
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    rewind(fp);

    char* buffer = malloc(size + 1);
    fread(buffer, 1, size, fp);
    buffer[size] = '\0';
    fclose(fp);
    return buffer;
}

int main(int argc, char** argv) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <source.shl>\n", argv[0]);
        return 1;
    }

    // ソースコードを読み込む
    char* code = read_file(argv[1]);

    // 字句解析 → 構文解析
    const char* src = code;
    Token* tok;
    while ((tok = next_token(&src))->type != TOKEN_EOF) {
        // TODO: parserに渡してAST構築する
        if (tok->type == TOKEN_PRINT) {
            // 仮の処理(AST構築前)
            Token* arg = next_token(&src);
            if (arg->type == TOKEN_STRING) {
                printf("%s\n", arg->text);
            } else if (arg->type == TOKEN_NUMBER) {
                printf("%s\n", arg->text);
            }
            free(arg->text);
            free(arg);
        }
        free(tok->text);
        free(tok);
    }

    free(code);
    return 0;
}

まずread_file関数でファイル全体を文字列としてメモリに読み込みC文字列にし、バッファの確保を行う。
次にmain関数でコマンド引数を確認する。print文を見つけたら次のトークンを読み取り出力される。

実行方法

実際にhello worldを出力してみる。
exampleファイルの中にhello.shl作成する。

hello.shl
print "Hello, World"

次に以下のコマンドを実行する。

bash
gcc src/*.c -o shelly
./shelly examples/hello.shl

現状print文のみの実装しかされいないが、shellyが動くことを確認できた。

今後の展望

  1. 言語使用の拡張
  2. 型システムの導入
  3. エラー処理の強化
  4. 実行環境の拡張
  5. 標準ライブラリの拡充
  6. 独自の特徴
  7. インストーラの実装

取り合えず動く状態まで作ることができたので、これから育てていこうと思う。今回のshellyを生み出すために生成AIを用いているため非効率な部分や間違いがあると思うが、これから修正アップデートを行うつもりだ。

もしアドバイス等あれば気軽に送ってください。

Discussion