5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

変数のスコープを実装したら、Rustの気持ちがわかった Part3

Last updated at Posted at 2025-12-09

Rustで作る自作コンパイラ入門シリーズ
Part1【字句解析】 | Part2【構文解析】 | Part3【意味解析】 | Part4【コード生成】 | Part5【完結】


はじめに

前回まででソースコードをASTに変換できるようになりました。

でもASTだけでは、こんなエラーは検出できません:

fn main() {
    return x;  // x って何?
}
fn add(a, b) { return a + b; }
fn main() {
    return add(1);  // 引数が足りない!
}

今回は 意味解析(Semantic Analysis) を実装して、こういうエラーを検出します!


目次

  1. 意味解析とは
  2. スコープとは
  3. シンボルテーブルを実装する
  4. エラー検出を実装する
  5. 動かしてみる

意味解析とは

意味解析(Semantic Analysis)は、プログラムの意味的な正しさをチェックします。

構文解析との違い

フェーズ チェック内容
構文解析 文法的に正しいか
意味解析 意味的に正しいか

例:

// 構文的にはOK、意味的にNG
fn main() {
    return undefined_variable;
}

構文解析は通るけど、意味解析で「変数が未定義」とエラーになります。

今回チェックする内容

  1. 未定義変数の使用
  2. 未定義関数の呼び出し
  3. 引数の数の不一致
  4. 変数の重複定義
  5. main関数の存在

スコープとは

スコープ(Scope) は、変数が有効な範囲のことです。

fn main() {
    let x = 10;        // xはここから有効
    
    if true {
        let y = 20;    // yはこのブロック内でのみ有効
        println(x);    // xは見える
    }
    
    println(x);        // xは見える
    println(y);        // エラー!yはスコープ外
}

スコープの入れ子

┌─────────────────────────────────┐
│ グローバルスコープ               │
│                                 │
│   ┌───────────────────────────┐ │
│   │ main関数のスコープ         │ │
│   │ x = 10                    │ │
│   │                           │ │
│   │   ┌─────────────────────┐ │ │
│   │   │ ifブロックのスコープ │ │ │
│   │   │ y = 20               │ │ │
│   │   └─────────────────────┘ │ │
│   │                           │ │
│   └───────────────────────────┘ │
│                                 │
└─────────────────────────────────┘

シンボルテーブルを実装する

スコープを管理するためのシンボルテーブルを実装します。

変数情報

/// 変数の情報
#[derive(Debug, Clone)]
pub struct VarInfo {
    pub name: String,
    pub is_param: bool,  // 関数パラメータかどうか
}

スコープ構造体

/// スコープ(変数の有効範囲)
#[derive(Debug)]
pub struct Scope {
    variables: HashMap<String, VarInfo>,
    parent: Option<Box<Scope>>,  // 親スコープへの参照
}

親スコープへの参照がポイント!これで入れ子構造を表現します。

スコープの操作

impl Scope {
    /// 新しいグローバルスコープを作成
    pub fn new() -> Self {
        Self {
            variables: HashMap::new(),
            parent: None,
        }
    }
    
    /// 子スコープを作成
    pub fn child(parent: Scope) -> Self {
        Self {
            variables: HashMap::new(),
            parent: Some(Box::new(parent)),
        }
    }
    
    /// 変数を定義
    pub fn define(&mut self, name: &str, is_param: bool) {
        self.variables.insert(name.to_string(), VarInfo { ... });
    }
    
    /// 変数を検索(親スコープも探す)
    pub fn lookup(&self, name: &str) -> Option<&VarInfo> {
        if let Some(var) = self.variables.get(name) {
            Some(var)
        } else if let Some(ref parent) = self.parent {
            parent.lookup(name)  // 親スコープを再帰的に検索
        } else {
            None
        }
    }
}

lookup再帰的に親を辿るのがポイント!

関数テーブル

関数も同様に管理:

#[derive(Debug)]
pub struct FunctionTable {
    functions: HashMap<String, FunctionInfo>,
}

#[derive(Debug, Clone)]
pub struct FunctionInfo {
    pub name: String,
    pub param_count: usize,  // 引数の数
}

エラー検出を実装する

アナライザーの構造

pub struct Analyzer {
    function_table: FunctionTable,
    errors: Vec<AnalyzeError>,
}

エラーは即座に失敗せず、全て集めてから報告します。

解析の流れ

pub fn analyze(&mut self, program: &Program) -> AnalyzeResult<()> {
    // フェーズ1: 全関数を登録(前方参照のため)
    for func in &program.functions {
        self.function_table.define(&func.name, func.params.len());
    }
    
    // フェーズ2: 各関数の本体を解析
    for func in &program.functions {
        self.analyze_function(func)?;
    }
    
    // main関数の存在チェック
    if self.function_table.lookup("main").is_none() {
        self.errors.push(AnalyzeError::new(
            "Function 'main' is not defined"
        ));
    }
    
    // エラーがあれば報告
    if !self.errors.is_empty() {
        return Err(...);
    }
    
    Ok(())
}

フェーズ1で先に全関数を登録することで、相互再帰などの前方参照に対応!

式の解析

fn analyze_expr(&mut self, expr: &Expr, scope: &Scope) -> AnalyzeResult<()> {
    match expr {
        Expr::Number(_) | Expr::Bool(_) => Ok(()),  // リテラルはOK
        
        Expr::Var(name) => {
            // 変数がスコープにあるか確認
            if scope.lookup(name).is_none() {
                self.errors.push(AnalyzeError::new(
                    format!("Undefined variable '{}'", name)
                ));
            }
            Ok(())
        }
        
        Expr::Call { name, args } => {
            // 関数が存在するかチェック
            match self.function_table.lookup(name) {
                Some(func_info) => {
                    // 引数の数をチェック
                    if args.len() != func_info.param_count {
                        self.errors.push(AnalyzeError::new(
                            format!(
                                "Function '{}' expects {} arguments, but got {}",
                                name, func_info.param_count, args.len()
                            )
                        ));
                    }
                }
                None => {
                    self.errors.push(AnalyzeError::new(
                        format!("Undefined function '{}'", name)
                    ));
                }
            }
            
            // 引数も解析
            for arg in args {
                self.analyze_expr(arg, scope)?;
            }
            
            Ok(())
        }
        
        // ... 他のケース
    }
}

文の解析(スコープ管理)

fn analyze_stmt(&mut self, stmt: &Stmt, mut scope: Scope) -> AnalyzeResult<Scope> {
    match stmt {
        Stmt::Let { name, value } => {
            // 値を先に解析(自己参照を防ぐ)
            self.analyze_expr(value, &scope)?;
            
            // 変数を定義
            if scope.is_defined_here(name) {
                self.errors.push(AnalyzeError::new(
                    format!("Variable '{}' is already defined", name)
                ));
            } else {
                scope.define(name, false);
            }
        }
        
        Stmt::If { cond, then_body, else_body } => {
            self.analyze_expr(cond, &scope)?;
            
            // then節は新しいスコープ
            let then_scope = Scope::child(scope);
            let then_scope = self.analyze_block(then_body, then_scope)?;
            
            // 元のスコープを取り戻す
            scope = *then_scope.parent.unwrap();
            
            // else節も同様
            // ...
        }
        
        // ... 他のケース
    }
    
    Ok(scope)
}

ブロックに入るときに新しいスコープを作成し、出るときに親に戻る


動かしてみる

正しいプログラム

fn add(a, b) {
    return a + b;
}

fn main() {
    let x = 10;
    let y = 20;
    let result = add(x, y);
    return result;
}

出力:

✅ Analysis passed! (2 function(s), 44 token(s))

エラーのあるプログラム

1. 未定義変数

fn main() {
    return x;
}
❌ Semantic error:
   Undefined variable 'x'

2. 未定義関数

fn main() {
    let x = foo();
    return x;
}
❌ Semantic error:
   Undefined function 'foo'

3. 引数の数が違う

fn add(a, b) {
    return a + b;
}
fn main() {
    return add(1);  // 引数1つしかない!
}
❌ Semantic error:
   Function 'add' expects 2 arguments, but got 1

4. main関数がない

fn foo() {
    return 42;
}
❌ Semantic error:
   Function 'main' is not defined

テスト

#[test]
fn test_undefined_variable() {
    let result = analyze_source(r#"
        fn main() {
            return x;
        }
    "#);
    assert!(result.is_err());
    assert!(result.unwrap_err().contains("Undefined variable 'x'"));
}

#[test]
fn test_scope_shadowing() {
    // ブロックスコープでの再定義はOK(シャドウイング)
    let result = analyze_source(r#"
        fn main() {
            let x = 10;
            if true {
                let x = 20;  // 新しいスコープなのでOK
            }
            return x;
        }
    "#);
    assert!(result.is_ok());
}

テスト実行:

cargo test
running 19 tests
test analyzer::tests::test_assign_to_undefined ... ok
test analyzer::tests::test_duplicate_variable ... ok
test analyzer::tests::test_no_main ... ok
test analyzer::tests::test_scope_shadowing ... ok
test analyzer::tests::test_undefined_function ... ok
test analyzer::tests::test_undefined_variable ... ok
test analyzer::tests::test_valid_program ... ok
test analyzer::tests::test_wrong_arg_count ... ok
... (lexer, parser tests) ...

test result: ok. 19 passed; 0 failed

Rustの気持ちがわかった

意味解析を実装してみて、Rustのコンパイラがやっていることが少しわかりました。

Rustが厳しい理由

fn main() {
    let x;
    println!("{}", x);  // エラー: 初期化前に使用
}

Rustは「初期化されていない変数」もエラーにします。
今回作ったコンパイラではそこまでやっていませんが、同じ仕組みで実装できます。

借用チェッカーの気持ち

fn main() {
    let s = String::from("hello");
    let r = &s;
    drop(s);  // エラー: 借用されている間は解放できない
    println!("{}", r);
}

スコープと変数の状態を追跡することで、こういうチェックも可能になります。


まとめ

今回やったこと

項目 内容
シンボルテーブル スコープの入れ子管理
関数テーブル 関数の登録と引数数チェック
エラー検出 未定義変数/関数、引数不一致
コード量 約200行

学び

  1. スコープは木構造

    • 親への参照で入れ子を表現
  2. 2パスが必要

    • 1パス目: 関数登録
    • 2パス目: 本体解析
  3. エラーは集める

    • 1つ見つけたら終わりじゃなく、全部報告

次回予告

Part4ではコード生成を実装します!

ASTから実際に動くコードを生成します。ついに...

fn main() {
    return 42;
}

が本当に 42 を返すようになる!

スタックマシンのコード生成、お楽しみに!

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?