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

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

言語とコンパイラを作る

承知いたしました。機能を絞り込み、よりシンプルなコンパイラと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/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
作成。
-
-
字句解析器 (Lexer) の実装とテスト (2/10)
-
lexer/lexer.go
,lexer/lexer_test.go
(文字列リテラル関連を除外)
-
-
AST ノードの拡充 (3/10)
-
ast/ast.go
,ast/ast_test.go
(文字列、配列、ハッシュ、関数リテラル関連を除外)
-
-
構文解析器 (Parser) の実装とテスト - 基本部分 (4/10)
-
parser/parser.go
,parser/parser_test.go
(let文、return文(不要なら削除)、式文。文字列、配列、ハッシュ、関数関連を除外)
-
-
構文解析器 (Parser) の実装とテスト - 式の解析 (5/10)
-
parser/parser.go
,parser/parser_test.go
(前置・中置演算子、真偽値、グループ化された式、if式。呼び出し式、配列、ハッシュ関連を除外)
-
-
バイトコード (Opcode) 定義と命令生成 (6/10)
-
code/code.go
: オペコードの定義 (関数呼び出し、配列、ハッシュ関連を除外)、命令フォーマット、Make
関数を実装。 -
code/code_test.go
:Make
関数のテスト、命令のエンコード・デコードテスト。
-
-
コンパイラの実装とテスト - 基本式とシンボルテーブル (7/10)
-
compiler/symbol_table.go
: シンボルテーブルの実装 (グローバルスコープのみ)。 -
compiler/symbol_table_test.go
: シンボルテーブルのテスト。 -
compiler/compiler.go
: コンパイラ構造体、Compile
メソッド、整数・真偽値リテラル、二項演算子、前置演算子、if式のコンパイル。 -
object/object.go
:CompiledInstruction
のようなバイトコードを保持するオブジェクトを追加。 -
compiler/compiler_test.go
: 対応するテストケース。
-
-
コンパイラの実装とテスト - 変数束縛と
puts
(8/10)-
compiler/compiler.go
:let
文、識別子参照のコンパイル。puts
関数のための特別なオペコードまたは処理。 -
compiler/compiler_test.go
: 対応するテストケース。
-
-
仮想マシン (VM) の実装とテスト (9/10)
-
vm/vm.go
: VM構造体、スタック、Run
メソッド。整数演算、比較演算、論理演算、OpConstant
,OpPop
,OpAdd
などの基本オペコード処理。if文の条件ジャンプ (OpJumpNotTruthy
,OpJump
)、グローバル変数 (OpSetGlobal
,OpGetGlobal
)、puts
用のOpCallBuiltin
(仮) などの処理。 -
vm/vm_test.go
: 対応するテストケース。
-
-
REPL の実装と全体統合 (10/10)
-
repl/repl.go
: REPL の実装。 -
main.go
: REPL 起動処理。 - 手動での動作確認。
- README の更新、
knowledge.mdc
の更新。
-
この計画で進めてよろしいでしょうか? よろしければ、ステップ 1/10 「プロジェクト初期化と基本型定義」を開始します。
(以前の day51_monkey_interpreter_go
ディレクトリは既に削除済みという認識です。)

はい、その理解は基本的に正しいです!とても良い観察です。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を使用します。