(静的)型システムの背景とRustによるスタックマシン上での実装
1. 本記事の目標・対象者
対象者
- Rust Bookの内容を一通り理解している人 (最低限: Rustコードを読めて、それをRustの文法や特性にあてはめ説明できる程度)
- 中間コード言語、ネイティブコード言語の仕組みとコンパイルの流れを理解している人
目標
- 静的型付けの意義を理解すること
- Rustによる独自言語 (Rustackとする) 静的型付けシステムの実装
実装はいくつかの小課題に分け、次のような形式で進めていきます。
ウォームアップ: 課題 0-1
次のプログラムが hello を出力するように指定箇所を書き換えよ。
fn main(){
let /*YOUR CODE HERE*/ s = String::from("hell");
pluso( /*YOUR CODE HERE*/s);
println!("{}", s);
}
fn pluso(some:/*YOUR CODE HERE*/ ){
some.push_str("o");
}
課題 0-1 の解説・回答例
回答例:
fn main(){
let mut s = String::from("hell");
pluso(&mut s);
println!("{}", s);
}
fn pluso(some: &mut String ){
some.push_str("o");
}
解説:
s への可変参照を作成し、pluso 関数に借用させる必要があります。
本記事ではこのような形式で実装を進めていきます。
2. (静的)型付けの意義とは
適切性 (正しいか?) の検証
要するに「きちんとコンパイルするため」です。実社会で起きるバグの多くは、基本的に静的型付けシステムで検知可能なものです (逆に動的型付けシステムの自由さがこれを生んだともいえます)。
これが重要な理由は以下の通りです。
- 規模の問題: 運用されるソフトウェアによってはコードは何万行、何十万行と膨らむことがあります。それらを個別にテスト (実行) するのは現実的ではありません。あらかじめ静的に捌かれていてほしいという需要があります。
-
網羅性の問題: 動的型付けシステム (実行時に型が決まるシステム) では、例えば二変数の関数
f(x,y)を定義した時に、すべてのx, yの取り得る型の組み合わせだけテストしなければ、テストの網羅性を満たしません。
今日、JavaScriptに静的型付け (後付け) を導入したTypeScriptや、Pythonの型アノテーションが登場していますが、これらはあくまで動的な型システムに対する「後付け」であり、元々あった問題を根本から刈り取るものではありません。最終的には動的言語として変換し、扱わざるを得ないためです。
実行最適化
動的型言語では、よく「複数の型を持ったデータ構造」を扱います。
一様な型の大量データを処理するとき、複数の型を持ちうるデータ構造はパフォーマンスの面からあまり好ましくありません (持ってしまう可能性があるので、本来は使わないデータ型でも、不必要に考慮してデータ構造を複雑にするため)。
その点、あらかじめコンパイル時に型を決められる静的型言語であれば、単純で最適なデータ構造を実現できます。
3. 型チェックシステムの構成概要
本章では、これから実装する静的型付けシステムがどのような原理で動作するのか、その設計図を解説します。
3.1 スタックマシンとは何か?
今回対象とする言語は、PostScriptやForth、あるいはJavaのバイトコードのような「スタックマシン」アーキテクチャを採用しています。
一般的なプログラミング言語 (CやRustなど) の式 1 + 2 は、スタックマシンでは 逆ポーランド記法 (RPN) を用いて 1 2 + と記述されます。
- 動作原理 (LIFO): データは「スタック」と呼ばれる積み上げ式の構造に保存されます。最後に入れたもの (Last-In) が最初に取り出されます (First-Out)。
- 計算の流れ:
-
1をスタックに積む (Push) -
2をスタックに積む (Push) -
+(演算子) が現れると、スタックから2と1を取り出し (Pop)、計算結果3をスタックに積む (Push)。
通常のインタプリタは「値 (10や20)」をスタックに積んで計算を行いますが、今回作成する型チェッカーは、「型 (IntやBool)」をスタックに積んでシミュレーションを行います。
-
実行時:
1020+→ スタックには30が残る。 -
型チェック時:
IntInt+→ スタックにはIntが残る。
もし Int Bool + のような命令が来たら、その時点で「型エラー」としてコンパイルを停止させるのが、このシステムの役割です。
3.2 Rustackで扱う「型」の種類
この言語の型システム (Universe) は、以下の4つの型で構成されます。これらはRustの enum Type で表現されます。
-
Int(整数型): 数値リテラル (10,5など) や、算術演算の結果。 -
Bool(真偽値型):true,falseや、比較演算 (eq) の結果。制御構文 (if) の条件として使用される。 -
Sym(シンボル型): 変数名そのものを表す。例えば/xというトークンは、「xという名前 (シンボル)」としてスタックに積まれる。これはdef(変数定義) の際に「どの変数に代入するか」を指定するために使う。 -
Block(ブロック型):{ ... }で囲まれたコードの塊です。スタックマシンでは、「プログラムコードそのもの」もデータとしてスタックに積むことができます。if文は、スタックから「Trueの時に実行するブロック」と「Falseの時に実行するブロック」を取り出して実行します。
3.3 型チェックの全体フロー
型チェッカー (check 関数) は、ソースコード (トークン列) を先頭から順に読み込み、以下のルールに従って「型スタック」と「型環境」を更新していきます。
- リテラルの処理:
- 数値
42が来たら、スタックにIntを積む。 - 真偽値
trueが来たら、スタックにBoolを積む。
- 変数の処理:
- 変数
xが現れたら、型環境 (TypeEnv) を検索する。 - 環境の中に
x: Intという記録があれば、スタックにIntを積む。未定義ならエラーにする。
- 演算子の処理:
-
+やeqなどの演算子が来たら、スタックから必要な数だけ型を取り出す (Pop)。 - 取り出した型が期待通りかチェックする (例:
+なら両方Intか?)。 - 正しければ、結果の型をスタックに積む (Push)。
- 制御構文 (
if) の処理:
-
ifはスタックから「条件」と「2つの分岐ブロック」を取り出す。 - 分岐のシミュレーション: TrueルートとFalseルートの両方で、ブロック内のコードを仮実行する。
- 単一化 (Unification): 両方のルートを実行した結果、最終的なスタックの状態 (型の並び) が一致するかを確認する。一致しなければ型矛盾としてエラーにする。
4. 型宣言
この言語で扱う「型」と、ソースコードの位置情報を示す「Span」を定義します。
課題 4-1: 型定義の実装
この言語では Int, Bool に加え、変数名を表すシンボル、そしてコードブロック自体も型として扱います。以下の enum Type を完成させてください。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub line: usize,
pub col: usize,
}
// 課題: ここにType列挙型を定義してください
// ヒント: Int, Bool, Sym(String), Block(Vec<Token>) が必要です
/* YOUR CODE HERE */
課題 4-1 の解説・回答
スタックマシンでは、コードブロック自体もデータとしてスタックに積まれるため、Block型がトークン列を持つ再帰的な構造になります。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Int,
Bool,
// 変数定義のために、名前そのものをスタックに積めるようにする
Sym(String),
// ブロックは中身のトークン列を持ち、実行時に展開される
Block(Vec<Token>),
}
課題 4-2: トークン (Token) の定義
次に、パーサー (字句解析器) から渡されるトークンの構造を定義します。
スタックマシンでは、/x (定義用シンボル) と x (参照用シンボル) を区別する必要があります。
以下の TokenKind 定義にある /* YOUR CODE HERE */ を埋めてください。
要件:
-
Sym: 変数参照 (例:x)。値をスタックに積むために環境を検索する対象。 -
Quoted: クォートされたシンボル (例:/x)。名前そのものをスタックに積む対象。 -
Block: 中括弧{ ... }で囲まれたコードブロック。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Int(i32),
Bool(bool),
Op(String),
/* YOUR CODE HERE */
}
課題 4-2 の解説・回答
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Int(i32),
Bool(bool),
Op(String),
Sym(String),
Quoted(String),
Block(Vec<Token>)
}
-
Quoted: Rustackでいう/xのような、宣言されただけの名前です。これ自体を実行しても値は得られず、名前として扱われます。 -
Sym: Rustackでは定義された値を参照・実行するための型です。
もし Quoted がないと、x 10 def としたときに x が定義されていることになり、実際には未定義なのに参照しようとしてエラーになってしまいます (定義時は名前そのものが必要)。
課題 4-3: 型環境 (TypeEnv) の構築
最後に、変数の型情報を保持する「型環境」を定義します。
変数名 (String) をキーとし、その変数の型 (Type) を値とするハッシュマップが必要です。
標準ライブラリの HashMap を使用して TypeEnv 構造体を完成させてください。
use std::collections::HashMap;
// === 型環境 ===
#[derive(Clone)]
struct TypeEnv {
/* YOUR CODE HERE */
// ヒント: vars: HashMap<String, Type>,
}
impl TypeEnv {
fn new() -> Self {
/* YOUR CODE HERE */
// ヒント: Self { vars: HashMap::new() }
}
}
課題 4-3 の解説・回答
implによって TypeEnv にメソッドを作り、コンストラクタとしての機能 (ハッシュマップ初期化) を作ります。
use std::collections::HashMap;
// === 型環境 ===
#[derive(Clone)]
struct TypeEnv {
vars: HashMap<String, Type>,
}
impl TypeEnv {
fn new() -> Self {
Self { vars: HashMap::new() }
}
}
5. 式と文の型チェック
次に、リテラル (値)、変数参照、および基本演算子のチェックロジックを実装します。
課題 5-1: リテラルのスタック操作
check 関数のメインループ内で、整数と真偽値のリテラルが来た際、対応する型をスタックに積む処理を記述してください。
check の引数および返り値の型は以下の通りです。
fn check(tokens: &[Token], env: &mut TypeEnv, mut stack: Vec<Type>) -> Result<Vec<Type>, String>
fn check(...) {
for token in tokens {
match &token.kind {
TokenKind::Int(_) => /* YOUR CODE HERE */,
TokenKind::Bool(_) => /* YOUR CODE HERE */,
TokenKind::Quoted(s) => /* YOUR CODE HERE */,
TokenKind::Block(inner_tokens) => /* YOUR CODE HERE */,
//..
課題 5-1 の解説・回答
fn check(...) {
for token in tokens {
match &token.kind {
TokenKind::Int(_) => stack.push(Type::Int),
TokenKind::Bool(_) => stack.push(Type::Bool),
TokenKind::Quoted(s) => stack.push(Type::Sym(s.clone())),
TokenKind::Block(inner_tokens) => stack.push(Type::Block(inner_tokens.clone())),
//..
-
Int,Boolでは値を束縛する必要がない (_) ので、そのまま型を Push します。 -
Quoted,Blockでは&Stringや&Vecで借用しているので、所有権を考慮して.clone()をつけてスタックに積みます。
課題 5-2: 変数参照の解決
トークンが Sym(name) (変数利用) の場合、型環境 env からその変数の型を検索し、スタックに積む処理を記述してください。定義されていない場合はエラーを返します。
//..from 5-1//
TokenKind::Sym(name) => {
if let Some(ty) = env.vars.get(name) {
/* YOUR CODE HERE */
} else {
return Err(format!("Undefined variable '{}' at {:?}", name, token.span));
}
//..to 5-3//
課題 5-2 の解説・回答
//..from 5-1//
TokenKind::Sym(name) => {
if let Some(ty) = env.vars.get(name) {
stack.push(ty.clone())
} else {
return Err(format!("Undefined variable '{}' at {:?}", name, token.span));
}
//..to 5-3//
env.vars.get(name) は Option<&Type> を返します。ty は借用されている状態なので、スタックに積むために .clone() が必要です。
課題 5-3: 加算演算子 (+) の型安全性
+ 演算子が来た時の処理です。スタックから2つの要素を取り出し、両方が Int である場合のみ、結果の Int をスタックに積むロジックを実装してください。
//..from 5-2//
TokenKind::Op(op) => match op.as_str() {
"+" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
/* YOUR CODE HERE */
// lhsとrhsが共にType::IntならType::Intをpush、そうでなければErr
},
//...to 5-4//
課題 5-3 の解説・回答
//..from 5-2//
TokenKind::Op(op) => match op.as_str() {
"+" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
if lhs == Type::Int && rhs == Type::Int {
stack.push(Type::Int);
} else {
return Err(format!("Type mismatch at {:?}", token.span));
}
},
//...to 5-4//
課題 5-4: 比較演算子 (eq)
eq 演算子は、スタックから2つの要素を取り出し、型が一致している場合のみ比較可能として Bool を返します。
/...from 5-3/
"eq" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
/* YOUR CODE HERE */
// 型(discriminant)が同じならType::Boolをpush
},
/... to 5-5/
課題 5-4 の解説・回答
/...from 5-3/
"eq" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
if std::mem::discriminant(&lhs) == std::mem::discriminant(&rhs) {
stack.push(Type::Bool);
} else {
return Err(format!("Type wrong Err in equal at {:?}", token.span));
}
},
/... to 5-5/
std::mem::discriminant は、enumのバリアント (列挙子) の一意識別子を返します。これを使うことで、中身の値に関わらず「型 (バリアント) が同じか」を判定できます。
エラー出力に token.span を含めることで、ソースコードのどこに誤りがあるかを報告できます。
課題 5-5: 変数定義 (def)
/x 10 def のように使います。スタックから「値」と「変数名 (Sym型)」を取り出し、環境 env に登録する処理を実装してください。
//...from 5-4//
"def" => {
let value_type = stack.pop().ok_or("Stack underflow")?;
let name_type = stack.pop().ok_or("Stack underflow")?;
if
/* YOUR CODE HERE */
// name_typeがType::Sym(s)なら、env.varsに(s, value_type)をinsert
},
//...to 6-1//
課題 5-5 の解説・回答
//...from 5-4//
"def" => {
let value_type = stack.pop().ok_or("Stack underflow")?;
let name_type = stack.pop().ok_or("Stack underflow")?;
if let Type::Sym(s) = name_type {
env.vars.insert(s, value_type);
} else {
return Err(format!("def requires a symbol name (/x), got {:?} at {:?}", name_type, token.span));
}
},
//...to 6-1//
if let 構文で Sym バリアントから文字列を取り出し、それをキーとして環境に登録します。
6. 組み合わせ関数のチェック
静的型付けシステムで最も重要な、分岐 (if) における型の整合性チェックを行います。
課題 6-1: if の前提条件チェック
if はスタックから [条件, Trueブロック, Falseブロック] を消費します。これらをポップし、条件が Bool 型であることを確認してください。
//...from 5-5//
"if" => {
let false_node = stack.pop().ok_or("Stack underflow")?;
let true_node = stack.pop().ok_or("Stack underflow")?;
let cond = stack.pop().ok_or("Stack underflow")?;
/* YOUR CODE HERE */ // condがType::Boolでなければエラー
//...to 6-2//
課題 6-1 の解説・回答
//...from 5-5//
"if" => {
let false_node = stack.pop().ok_or("Stack underflow")?;
let true_node = stack.pop().ok_or("Stack underflow")?;
let cond = stack.pop().ok_or("Stack underflow")?;
if cond != Type::Bool {
return Err(format!("if condition must be Bool at {:?}", token.span));
}
//...to 6-2//
課題 6-2: 分岐のシミュレーション
TrueルートとFalseルート、それぞれのブロックを実行した場合のスタックの状態を計算します。
ヒント: check 関数を再帰呼び出しします。その際、環境とスタックは複製 (clone) して渡す必要があります。
//from 6-1//
// ブロックの中身を取り出す(実装済み)
let (true_tokens, false_tokens) = match (true_node, false_node) {
(Type::Block(t), Type::Block(f)) => (t, f),
_ => return Err("if requires two blocks".to_string()),
};
/* YOUR CODE HERE */
// let true_stack_result = check(...)?;
// let false_stack_result = check(...)?;
//to 6-3//
課題 6-2 の解説・回答
//from 6-1//
let (true_tokens, false_tokens) = match (true_node, false_node) {
(Type::Block(t), Type::Block(f)) => (t, f),
_ => return Err("if requires two blocks".to_string()),
};
let true_stack_result = check(&true_tokens, &mut env.clone(), stack.clone())?;
let false_stack_result = check(&false_tokens, &mut env.clone(), stack.clone())?;
//to 6-3//
課題 6-3: スタックの単一化 (Unification)
最後に、分岐の両ルートから返ってきたスタックの状態 (true_stack_result と false_stack_result) が完全に一致するか検証してください。一致すればそのスタックを採用し、不一致なら型エラーとします。
//...from 6-2//
/* YOUR CODE HERE */
// スタックが一致するか確認し、一致すれば現在のstackを更新
},
課題 6-3 の解説・回答
//...from 6-2//
if true_stack_result != false_stack_result {
return Err(format!(
"Type Mismatch in branches at {:?}:\n True path: {:?}\n False path: {:?}",
token.span, true_stack_result, false_stack_result
));
}
// 整合性が取れたスタックを採用
stack = true_stack_result;
},
解説:
実行時にどちらの分岐に入るかは (型チェック時点では) 確定しません。したがって、TrueルートとFalseルートの両方をシミュレーションし、最終的なスタックの状態 (数と型) が一致しなければ、プログラムの型安全性が保証できないことになります。
例えば以下のようなコードで型エラーを検出できます:
// NG例: 分岐の結果の型が異なる
x y eq { 10 } { true } if
// Trueルート→Int, Falseルート→Bool でスタックが不一致となるためエラー
このチェックがあることで、if 文の後に続くコードが、どちらの分岐を通っても同じ型のスタック状態を前提にできます。
7. 最終コード
完成したソースコード全体です。
use std::collections::HashMap;
// === データ構造定義 ===
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub line: usize,
pub col: usize,
}
// [課題 4-1] 型列挙体の定義
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Int,
Bool,
Sym(String), // 変数定義用 (/x)
Block(Vec<Token>), // コードブロック ({ ... })
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenKind {
Int(i32),
Bool(bool),
Op(String),
Sym(String), // 変数参照 (x)
Quoted(String), // クォートされたシンボル (/x)
Block(Vec<Token>),
}
// === 型環境 ===
#[derive(Clone)]
struct TypeEnv {
vars: HashMap<String, Type>,
}
impl TypeEnv {
fn new() -> Self {
Self { vars: HashMap::new() }
}
}
// === 型チェッカー (Core Logic) ===
fn check(tokens: &[Token], env: &mut TypeEnv, mut stack: Vec<Type>) -> Result<Vec<Type>, String> {
for token in tokens {
match &token.kind {
// [課題 5-1] リテラルのスタック操作
TokenKind::Int(_) => stack.push(Type::Int),
TokenKind::Bool(_) => stack.push(Type::Bool),
// クォートされたシンボル(定義用)はSym型として積む
TokenKind::Quoted(s) => stack.push(Type::Sym(s.clone())),
// ブロック定義はBlock型として積む
TokenKind::Block(inner_tokens) => stack.push(Type::Block(inner_tokens.clone())),
// [課題 5-2] 変数参照の解決
TokenKind::Sym(name) => {
if let Some(ty) = env.vars.get(name) {
stack.push(ty.clone());
} else {
return Err(format!("Undefined variable '{}' at {:?}", name, token.span));
}
}
TokenKind::Op(op) => match op.as_str() {
// [課題 5-3] 加算演算子
"+" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
if lhs == Type::Int && rhs == Type::Int {
stack.push(Type::Int);
} else {
return Err(format!("Type mismatch: {:?} + {:?} at {:?}", lhs, rhs, token.span));
}
},
// [課題 5-4] 比較演算子
"eq" => {
let rhs = stack.pop().ok_or("Stack underflow")?;
let lhs = stack.pop().ok_or("Stack underflow")?;
// 型の判別子が同じなら比較可能とする
if std::mem::discriminant(&lhs) == std::mem::discriminant(&rhs){
stack.push(Type::Bool);
} else {
return Err(format!("Cannot compare different types at {:?}", token.span));
}
},
// [課題 5-5] 変数定義 (def)
"def" => {
let value_type = stack.pop().ok_or("Stack underflow (def needs value)")?;
let name_type = stack.pop().ok_or("Stack underflow (def needs name)")?;
if let Type::Sym(name_str) = name_type {
env.vars.insert(name_str, value_type);
} else {
return Err(format!("def requires a symbol name (/x), got {:?} at {:?}", name_type, token.span));
}
},
// [課題 6] 制御構文 (if)
"if" => {
let false_node = stack.pop().ok_or("Stack underflow (if needs false block)")?;
let true_node = stack.pop().ok_or("Stack underflow (if needs true block)")?;
let cond = stack.pop().ok_or("Stack underflow (if needs condition)")?;
// [課題 6-1] 条件チェック
if cond != Type::Bool {
return Err(format!("if condition must be Bool at {:?}", token.span));
}
let (true_tokens, false_tokens) = match (true_node, false_node) {
(Type::Block(t), Type::Block(f)) => (t, f),
_ => return Err("if requires two blocks".to_string()),
};
// [課題 6-2] 分岐シミュレーション (環境とスタックを複製して探索)
let true_stack_result = check(&true_tokens, &mut env.clone(), stack.clone())?;
let false_stack_result = check(&false_tokens, &mut env.clone(), stack.clone())?;
// [課題 6-3] 型の単一化 (Unification)
if true_stack_result != false_stack_result {
return Err(format!(
"Type Mismatch in branches at {:?}:\n True path: {:?}\n False path: {:?}",
token.span, true_stack_result, false_stack_result
));
}
// 整合性が取れたスタックを採用
stack = true_stack_result;
},
_ => return Err(format!("Unknown op '{}'", op)),
}
}
}
Ok(stack)
}
// === メイン関数 (動作確認用) ===
fn main() {
// テストコード:
// /x 10 def (x: Int を定義)
// /y 20 def (y: Int を定義)
// x y eq (Int == Int -> Bool)
// { x } { y } if (Bool ? Int : Int -> 最終スタックは [Int])
let tokens = vec![
Token { kind: TokenKind::Quoted("x".to_string()), span: Span { line: 1, col: 1 } },
Token { kind: TokenKind::Int(10), span: Span { line: 1, col: 4 } },
Token { kind: TokenKind::Op("def".to_string()), span: Span { line: 1, col: 7 } },
Token { kind: TokenKind::Quoted("y".to_string()), span: Span { line: 2, col: 1 } },
Token { kind: TokenKind::Int(20), span: Span { line: 2, col: 4 } },
Token { kind: TokenKind::Op("def".to_string()), span: Span { line: 2, col: 7 } },
Token { kind: TokenKind::Sym("x".to_string()), span: Span { line: 3, col: 1 } },
Token { kind: TokenKind::Sym("y".to_string()), span: Span { line: 3, col: 3 } },
Token { kind: TokenKind::Op("eq".to_string()), span: Span { line: 3, col: 5 } },
Token { kind: TokenKind::Block(vec![
Token { kind: TokenKind::Sym("x".to_string()), span: Span { line: 4, col: 3 } }
]), span: Span { line: 4, col: 1 } },
Token { kind: TokenKind::Block(vec![
Token { kind: TokenKind::Sym("y".to_string()), span: Span { line: 4, col: 7 } }
]), span: Span { line: 4, col: 5 } },
Token { kind: TokenKind::Op("if".to_string()), span: Span { line: 4, col: 10 } },
];
let mut env = TypeEnv::new();
match check(&tokens, &mut env, vec![]) {
Ok(final_stack) => println!("Check OK. Final Stack: {:?}", final_stack),
Err(e) => eprintln!("Check Failed: {}", e),
}
}