Closed6

100日チャレンジ day51 (Mokey言語と自作コンパイラ)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/7fccf9623851aa


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day51_monkey_compiler_go

riddle_tecriddle_tec

承知いたしました。機能を絞り込み、よりシンプルなコンパイラとVMを目指しましょう。

プロジェクトの仕様 (コンパイラ版 - 簡略版)

  • テーマ: Go による Monkey 言語コンパイラとVMの実装 (簡略版)
  • 主な学習ポイント:
    • プログラミング言語のコンパイラとVMの内部構造 (基礎)
    • バイトコードの設計と生成
    • スタックベースの仮想マシンの実装
    • Go によるデータ構造とアルゴリズムの実装
  • 機能要件:
    • 整数、真偽値リテラル
    • 算術演算 (+, -, *, /)
    • 比較演算 (<, >, ==, !=)
    • 論理演算 (!)
    • 変数束縛 (let 文 - グローバルスコープのみ)
    • if 式 (値を持つ必要はなし、条件分岐のみ)
    • 単純な組み込み関数: puts (引数を画面に出力)
    • REPL インターフェース (コンパイル -> VM実行)
  • 除外する機能:
    • 文字列リテラル
    • 関数定義とユーザー定義関数呼び出し (クロージャも含む)
    • 配列リテラルとインデックスアクセス
    • ハッシュリテラルとインデックスアクセス
    • 複雑な組み込み関数群
    • ローカルスコープの変数
  • 使用技術:
    • 言語: Go
    • パッケージ管理: Go Modules
    • テスト: Go 標準の testing パッケージ

この仕様変更に伴い、ディレクトリ構成と作業計画を更新します。

ディレクトリレイアウトと作成するファイル一覧 (予定 - 簡略版):

day51_monkey_compiler_go/
├── go.mod
├── go.sum
├── main.go            // REPL のエントリーポイント
├── token/
│   └── token.go         // トークンの定義
├── lexer/
│   └── lexer.go         // 字句解析器
│   └── lexer_test.go    // 字句解析器のテスト
├── ast/
│   └── ast.go           // ASTノードの定義
│   └── ast_test.go      // AST関連のテスト
├── parser/
│   └── parser.go        // 構文解析器
│   └── parser_test.go   // 構文解析器のテスト
├── code/
│   └── code.go          // バイトコードのオペコード定義、命令生成
│   └── code_test.go     // バイトコード関連のテスト
├── compiler/
│   └── compiler.go      // ASTからバイトコードへのコンパイラ
│   └── compiler_test.go // コンパイラのテスト
│   └── symbol_table.go  // シンボルテーブル (グローバルのみ)
│   └── symbol_table_test.go // シンボルテーブルのテスト
├── object/
│   └── object.go        // Monkey言語のオブジェクトシステムの定義 (VM用、整数と真偽値、Null、エラー程度)
├── vm/
│   └── vm.go            // スタックベース仮想マシン
│   └── vm_test.go       // VMのテスト
├── repl/
│   └── repl.go          // REPLの実装 (コンパイラとVMを使用)
└── README.md

作業順序 (ステップ - 簡略版):

  1. プロジェクト初期化と基本型定義 (1/10)

    • day51_monkey_compiler_go ディレクトリ作成
    • go mod init github.com/lirlia/100day_challenge_backend/day51_monkey_compiler_go
    • token/token.go, ast/ast.go, object/object.go (整数、真偽値、Null、エラーオブジェクト) を定義。
    • README.md にプロジェクト概要と本計画を記載。
    • PROGRESS.md 作成。
  2. 字句解析器 (Lexer) の実装とテスト (2/10)

    • lexer/lexer.go, lexer/lexer_test.go (文字列リテラル関連を除外)
  3. AST ノードの拡充 (3/10)

    • ast/ast.go, ast/ast_test.go (文字列、配列、ハッシュ、関数リテラル関連を除外)
  4. 構文解析器 (Parser) の実装とテスト - 基本部分 (4/10)

    • parser/parser.go, parser/parser_test.go (let文、return文(不要なら削除)、式文。文字列、配列、ハッシュ、関数関連を除外)
  5. 構文解析器 (Parser) の実装とテスト - 式の解析 (5/10)

    • parser/parser.go, parser/parser_test.go (前置・中置演算子、真偽値、グループ化された式、if式。呼び出し式、配列、ハッシュ関連を除外)
  6. バイトコード (Opcode) 定義と命令生成 (6/10)

    • code/code.go: オペコードの定義 (関数呼び出し、配列、ハッシュ関連を除外)、命令フォーマット、Make 関数を実装。
    • code/code_test.go: Make 関数のテスト、命令のエンコード・デコードテスト。
  7. コンパイラの実装とテスト - 基本式とシンボルテーブル (7/10)

    • compiler/symbol_table.go: シンボルテーブルの実装 (グローバルスコープのみ)。
    • compiler/symbol_table_test.go: シンボルテーブルのテスト。
    • compiler/compiler.go: コンパイラ構造体、Compile メソッド、整数・真偽値リテラル、二項演算子、前置演算子、if式のコンパイル。
    • object/object.go: CompiledInstruction のようなバイトコードを保持するオブジェクトを追加。
    • compiler/compiler_test.go: 対応するテストケース。
  8. コンパイラの実装とテスト - 変数束縛と puts (8/10)

    • compiler/compiler.go: let 文、識別子参照のコンパイル。puts 関数のための特別なオペコードまたは処理。
    • compiler/compiler_test.go: 対応するテストケース。
  9. 仮想マシン (VM) の実装とテスト (9/10)

    • vm/vm.go: VM構造体、スタック、Run メソッド。整数演算、比較演算、論理演算、OpConstant, OpPop, OpAddなどの基本オペコード処理。if文の条件ジャンプ (OpJumpNotTruthy, OpJump)、グローバル変数 (OpSetGlobal, OpGetGlobal)、puts 用の OpCallBuiltin (仮) などの処理。
    • vm/vm_test.go: 対応するテストケース。
  10. REPL の実装と全体統合 (10/10)

    • repl/repl.go: REPL の実装。
    • main.go: REPL 起動処理。
    • 手動での動作確認。
    • README の更新、knowledge.mdc の更新。

この計画で進めてよろしいでしょうか? よろしければ、ステップ 1/10 「プロジェクト初期化と基本型定義」を開始します。
(以前の day51_monkey_interpreter_go ディレクトリは既に削除済みという認識です。)

riddle_tecriddle_tec

はい、その理解は基本的に正しいです!とても良い観察です。Day51で実装したものと実際のプログラミング言語の違いを詳しく説明しますね。

Day51 Monkey言語の実行フロー

Monkey言語ソースコード
    ↓ (字句解析・構文解析・コンパイル)
独自バイトコード (OpConstant, OpAdd, OpPop等)
    ↓ (Go製VMで解釈実行)
Go言語の命令 (fmt.Println, strconv.ParseInt等)
    ↓ (Goコンパイラが事前に機械語変換済み)
機械語 (x86_64, ARM64等のCPU命令)
    ↓
CPU実行

実際のGo/C言語の実行フロー

Go言語の場合:

Go言語ソースコード
    ↓ (go build/go run でコンパイル)
機械語 (CPU固有のアセンブリコード)
    ↓
CPU直接実行

C言語の場合:

C言語ソースコード
    ↓ (gcc/clang等でコンパイル)
機械語 (CPU固有のアセンブリコード)
    ↓
CPU直接実行

主な違い

項目 Day51 Monkey Go/C言語
中間表現 独自バイトコード 機械語
実行方法 VM上でインタープリット CPU上で直接実行
性能 遅い(VMレイヤーがある) 速い(CPU直接実行)
ポータビリティ VMがあればどこでも動く CPU/OS固有にコンパイル必要

他言語の例

実際には、言語によって様々なアプローチがあります:

Java:

Java → バイトコード → JVM → (JITコンパイル) → 機械語 → CPU

Day51と似ているが、JVMは最適化されたJITコンパイルを行う

Python:

Python → バイトコード → Python VM (C実装) → 機械語 → CPU

JavaScript (V8):

JavaScript → バイトコード → V8エンジン → (JITコンパイル) → 機械語 → CPU

実際のCPU命令の例

Day51のMonkey言語で 1 + 2 を実行する場合:

Monkey独自バイトコード:

OpConstant 0    // 定数1をスタックにプッシュ
OpConstant 1    // 定数2をスタックにプッシュ  
OpAdd          // スタックから2つpopして足し算

実際のx86_64機械語だと:

mov rax, 1      ; レジスタraxに1を格納
add rax, 2      ; raxに2を加算

まとめ

ユーザーの理解は正確です:

Day51 Monkey: 独自opcode → Go製VM → 機械語 → CPU
Go/C言語: ソースコード → 機械語 → CPU(直接実行)

Day51で実装したのは教育目的のインタープリター型VMで、実際の本格的な言語処理系の基本概念を学ぶのに最適な実装でした。実用的な言語では性能のため、直接機械語にコンパイルするか、高度に最適化されたVMを使用します。

このスクラップは2ヶ月前にクローズされました