無駄と文化

実用的ブログ

Rust で Grass インタプリタを実装する

書くのも読むのも難しい Grass 言語のインタプリタを書きました。Rust 実装です。

github.com

cargo install --git https://github.com/todays-mitsui/rusty-grass.git でインストールして遊べます。

 

モチベーション

Grass 言語は言語仕様がカッチリしていて、かつ難解でいかにも面白げなのでいつかちゃんと理解していと考えていました。

web.archive.org

しかし、狙った文字列を標準出力に出すのも一筋縄ではいかない言語仕様なのでインタプリタを叩きながら理解するのもなかなか難しい。
ふと思ったのが『これインタプリタの実装を自分でやるほうが分かりよいのでは?』ということでした。

 

参考にしたのは Grass 言語の作者である UENO Katsuhiro さんの 公式ドキュメント と、 id:susisu さんの JavaScript 実装 Grass-JS です。

実際に実装をやってみると Grass コードのパーザや隠れた状態を表現するための構造体をすべて自作することになるので理解が深まりました。実装してよかった。

 

実行方法とサンプルコード

インストールが完了すると grass コマンドが使えるようになります。

$ grass path/to/progfile

とすると path/to/progfile のコードを実行できます。

リポジトリにはサンプルコードを含めています。例えば HelloWorld のコードは rusty-grass/example/helloworld.grass これです。

まとめ

難解で、楽しかった。

 

 

私からは以上です。

Debug トレイトを使いこなして構造体を自由自在に印字する

この記事は Rust Advent Calendar 2025 の15日目の記事です。
大遅刻、すみませんすみません。

 


Rust ではオリジナルの構造体を println!() で印字しようとしても、デフォルトでは印字できません。

// 二次元平面上の点を表現する構造体
struct Point {
    name: String,
    x: f32,
    y: f32,
}

fn main () {
    let a = Point {
        name: String::from("A"),
        x: 1.0,
        y: 2.0,
    };

    println!("{:?}", a);
//                   ^ `Point` cannot be formatted using `{:?}`
//                     because it doesn't implement `Debug`
}

{:?} で印字するには Debug トレイトが必要とのことで、derive 属性をつけて Debug を実装してあげれば印字可能になります。

  // 二次元平面上の点を表現する構造体
+ #[derive(Debug)]
  struct Point {
      name: String,
      x: f32,
      y: f32,
  }

  fn main () {
      let a = Point {
          name: String::from("A"),
          x: 1.0,
          y: 2.0,
      };

      println!("{:?}", a);
+     // => Point { name: "A", x: 1.0, y: 2.0 }
  }

#[derive(Debug)] するだけで実装できるのはお手軽でいいですね。

 

独自の形式で印字する

もう一歩踏み込んで構造体の意味に合わせた独自の形式での印字をやってみましょう。

struct Point { name, x, y } は「x座標, y座標を指定した二次元平面上の点」を表現しています。なので、 Point { name: "A", x: 1.0, y: 2.0 } を印字するときには A (1, 2) としてみるとどうでしょうか。
Debug トレイトは独自に impl することも可能です。やってみましょう。

+ use std::fmt::{Debug, Formatter, Result};
  
  // 二次元平面上の点を表現する構造体
- #[derive(Debug)]
  struct Point {
      name: String,
      x: f32,
      y: f32,
  }
  
+ impl Debug for Point {
+     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+         write!(f, "{} ({}, {})", self.name, self.x, self.y)
+     }
+ }
  
  fn main () {
      let a = Point {
          name: String::from("A"),
          x: 1.0,
          y: 2.0,
      };
      
      println!("{:?}", a);
-     // => Point { name: "A", x: 1.0, y: 2.0 }
+     // => A (1, 2)
  }

impl Debug for PointDebug::fmt メソッドを独自に実装しています。
やっていることは単純で、 write!() を呼び出して self.name, self.x, self.y をお好みの形式に印字しているだけです。

write!(f, "{} ({}, {})", self.name, self.x, self.y)

雰囲気としては、format!("{} ({}, {})", self.name, self.x, self.y) と同じことですね。

 

Debug トレイトは Formatter 構造体と密接に結びついている

ここまで読んで「つまり Debug::fmt メソッドは値を文字列化して返せばいいんだな」と思いませんでしたか?私は思ってました。
しかし println!() の結果をよく見てみると、同じ値に対してインデントが付与される場合があることに気づきます。

let piyo2 = vec!["piyo"; 2];

println!("{:?}", piyo2);
// => ["piyo", "piyo"]

println!("{:#?}", piyo2);
// => [
//     "piyo",
//     "piyo",
// ]

Debug::fmt メソッドの結果がただの文字列であれば、このような整形はできないはずです。
write!() による実装はお手軽ですが、Debug トレイトでできることの全てではありません。

次からもっと凝った実装例を見ていきましょう。

 

List 表示, Set 表示

組み込みの構造体に std::fmt::DebugList というものがあります。これを使うと Vec を format!() したときのような表示をユーザーが手軽に再現できます。

use std::fmt::{Debug, Formatter, Result};

struct MyVec<'a, T> {
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MyVec<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.debug_list()
         .entries(self.items.iter())
         .finish()
    }
}

fn main () {
    let v = MyVec { items: &vec!["hoge", "fuga"] };

    println!("{:?}", v);
    // => ["hoge", "fuga"]

    println!("{:#?}", v);
    // => [
    //     "hoge",
    //     "fuga",
    // ]
}

{:?} なら1行で、{:#?} なら複数行でインデントして印字されていますね。

実装は簡単です。 f.debug_list() が DebugList のインスタンスを返すのでこれを使います。
そのあと .entry(item).entries(items_iter) で要素を与えていくだけです。最後に .finish() すれば Result<(), std::fmt::Error> が得られるので、これをそのまま返します。

 

std::fmt::DebugList の仲間に std::fmt::DebugSet があります。
DebugList は [ ... ] 形式で印字してくれていました。DebugSet は { ... } 形式で値を印字してくれます。

Set 表示をしたいときには f.debug_set() を使います。

use std::fmt::{Debug, Formatter, Result};

struct MySet<'a, T> {
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MySet<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.debug_set()
            .entries(self.items.iter())
            .finish()
    }
}

fn main() {
    let s = MySet { items: &["hoge", "fuga"] };

    println!("{:?}", s);
    // => {"hoge", "fuga"}

    println!("{:#?}", s);
    // => {
    //     "hoge",
    //     "fuga",
    // }
}

 

構造体表示, タプル構造体表示

構造体 struct S { ... } やタプル構造体 struct T ( ... )format!() したときのような表示を再現することもできます。
std::fmt::DebugStruct, std::fmt::DebugTuple を使います。

use std::collections::HashMap;
use std::fmt::{Debug, Formatter, Result};

struct MyStruct<'a, K, V> {
    name: String,
    items: &'a HashMap<K, V>,
}

impl<'a, K: Debug + Ord, V: Debug> Debug for MyStruct<'a, K, V> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut entries: Vec<_> = self.items.iter().collect();
        entries.sort_by_key(|(k, _)| *k);

        let mut dbg = f.debug_struct(&self.name);
        for (ref k, ref v) in entries {
            dbg.field(&format!("{:?}", k), v);
        }
        dbg.finish()
    }
}

struct MyTuple<'a, T> {
    name: String,
    items: &'a [T],
}

impl<'a, T: Debug> Debug for MyTuple<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_tuple(&self.name);
        for item in self.items.iter() {
            dbg.field(item);
        }
        dbg.finish()
    }
}

fn main() {
    let s = MyStruct {
        name: String::from("S"),
        items: &HashMap::from([("hoge", "fuga")]),
    };
    println!("{:?}", s);
    // => S { "hoge": "fuga" }

    let t = MyTuple {
        name: String::from("T"),
        items: &["hoge", "fuga"],
    };
    println!("{:?}", t);
    // => T("hoge", "fuga")
}

構造体表示したい場合には f.debug_struct(name) からの .field(k, v) 、タプル構造体表示したい場合には f.debug_tuple(name) からの .field(v) するだけです。

 

応用例

これらの知識を使ってより高度な応用例を紹介します。

インデックス番号付きの List 表示

use std::fmt::{Debug, Formatter, Result};

struct IndexedList<'a, T>(&'a [T]);

impl<'a, T: Debug> Debug for IndexedList<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_list();
        for (i, item) in self.0.iter().enumerate() {
            dbg.entry(&IndexedItem(&(i, item)));
        }
        dbg.finish()
    }
}

struct IndexedItem<'a, T>(&'a T);

impl<'a, T: Debug> Debug for IndexedItem<'a, (usize, &'a T)> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(&self.0.0, f)?;
        write!(f, ": ")?;
        Debug::fmt(self.0.1, f)
    }
}

fn main() {
    let v = vec!["hoge", "fuga", "piyo"];

    println!("{:?}", v);
    // => ["hoge", "fuga", "piyo"]

    println!("{:?}", IndexedList(&v));
    // => [0: "hoge", 1: "fuga", 2: "piyo"]

    println!("{:#?}", IndexedList(&v));
    // => [
    //     0: "hoge",
    //     1: "fuga",
    //     2: "piyo",
    // ]
}

いつもの List 表示っぽくもありつつ、インデックス番号を添えてくれるのでどの要素が何番目かわかりやすくなります。

 

末尾要素を省略した List 表示

use std::fmt::{Debug, Formatter, Result};

struct EllipsisList<'a, T>(&'a [T]);

impl<'a, T: Debug> Debug for EllipsisList<'a, T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        let mut dbg = f.debug_list();
        let mut iter = self.0.iter();
        // 高々3要素まで表示し、残りがあれば省略表示する
        for _ in 0..3 {
            if let Some(item) = iter.next() {
                dbg.entry(item);
            } else {
                break;
            }
        }
        if iter.next().is_some() {
            dbg.entry(&Ellipsis);
        }
        dbg.finish()
    }
}

struct Ellipsis;

impl Debug for Ellipsis {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "...")
    }
}

fn main() {
    let v = vec![0, 1, 2, 3, 4, 5];
    
    println!("{:?}", v);
    // => [0, 1, 2, 3, 4, 5]

    println!("{:?}", EllipsisList(&v));
    // => [0, 1, 2, ...]

    println!("{:#?}", EllipsisList(&v));
    // => [
    //     0,
    //     1,
    //     2,
    //     ...,
    // ]
}

長い List を全部印字する必要がなく、先頭要素だけに興味がある場合はこのような方法も便利ですね。

ポイントは ... を印字するのに dbg.entry("..."); とするのではなく、ユニット構造体 Ellipsis を定義して dbg.entry(&Ellipsis); としているところです。
&strdyn Debug なので dbg.entry("..."); と書くことは可能ですが、これだと印字結果は "..." となり、余計なダブルクォートがついてしまいます。

 

まとめ

Rust の Debug トレイトは #[derive(Debug)]write!() でお手軽に使うこともできるし、カスタマイズしてこだわった表示にもできるのが魅力的ですね。

 

 

私からは以上です。

やってみよう!メモリリーク

メリークリスマス!
この記事は はてなエンジニア Advent Calendar 2025 の3日目の記事です。

大遅刻ですね。すごいことですよこれは。

 


さて、Rust ネタです。

ご存知の通り Rust には GC がありません。代わりに所有権システムがあり、所有権を持つ変数がスコープを抜けるときに値がドロップされ、メモリが解放されます。
とはいえ通常のドロップ処理を回避したり相互に参照する値を作って変数を無限に長生きさせると Rust でもメモリは漏れます。

今日はメモリリークする Rust コードを実際に書いて、書き味を確かめてみましょう。

 

Rc<T> と相互参照

素朴な参照カウンター式のガベージコレクターはどこからか値が参照されている限りメモリを解放しません。
そのため相互に参照する値を作ると参照カウンターが0にならず、回収されない値を作れます。

JavaScript 風の仮想言語で書くとこんな感じ。

class Node {
  next = null;
}

function main() {
  const a = new Node();
  const b = new Node();

  a.next = b;
  b.next = a;
}
// ↑↑ ここで main は a, b の参照を辞める
// しかし a は b を参照し続け、b は a を参照し続ける
// 結果として a, b の参照が残り続ける

main();

※ 実際の JavaScript ランタイムのガベージコレクターは賢いので、上記の実装でも適切にメモリを回収してくれます

 

Rust で上記のようなコードを書いてみましょう。
Rust には参照カウント方式のメモリ管理を提供する Rc<T> 型があります。ただし Rc<T> だけを素朴に使っても循環参照は作れません。

use std::rc::Rc;

struct Node {
    next: Option<Rc<Node>>,
}

fn main() {
    let a = Rc::new(Node { next: None });
    let b = Rc::new(Node { next: None });

    *a.next = Some(Rc::clone(&b));
//  ^^^^^^^ can't be dereferenced
//  error[E0614]: type `Option<Rc<Node>>` cannot be dereferenced
    *b.next = Some(Rc::clone(&a));
//  ^^^^^^^ can't be dereferenced
//  error[E0614]: type `Option<Rc<Node>>` cannot be dereferenced
}

Rc<T> は参照先を不変にするので上記のように可変参照を借用することはできません。

...ということが 書籍『プログラミング Rust』 に書いてあって。RefCell<T> による 内部可変 を使えばこの制限を回避できるとも書かれています。
やってみましょう。

use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    {
        let a = Rc::new(Node {
            next: RefCell::new(None),
        });
        let b = Rc::new(Node {
            next: RefCell::new(None),
        });

        *a.next.borrow_mut() = Some(Rc::clone(&b));
        *b.next.borrow_mut() = Some(Rc::clone(&a));
    }
    // ↑↑ ここで a, b のスコープ終わり "変数" が所有権を失う
    // しかし循環参照により Rc<Node> の "値" はドロップされずに残る
}

理屈ではこれでメモリが漏れるはず。とはいえ実際に目で見える形でないと面白くありませんね。
可視化してみましょう。

 

メモリリークを目で見る

やはりプログラムの実行中にメモリ消費が増えていく様を見るほうが実感が湧きますね。
もっと大きめのメモリをリークさせるプログラムを走らせつつ、ps u コマンドで RSS, VSZ あたりを観察してみましょう。

さきほどの Rust コードを改造します。

use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;

struct Node {
    #[allow(dead_code)]
    payload: Vec<u8>,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let a = Rc::new(Node {
                payload: vec![0u8; payload_bytes],
                next: RefCell::new(None),
            });

            let b = Rc::new(Node {
                payload: vec![1u8; payload_bytes],
                next: RefCell::new(None),
            });

            // 循環参照を作る
            *a.next.borrow_mut() = Some(Rc::clone(&b));
            *b.next.borrow_mut() = Some(Rc::clone(&a));
        }
        // ↑↑ ここで a, b のスコープ終わり "変数" が所有権を失う
        // しかし循環参照により Rc<Node> の "値" はドロップされずに残る

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

構造体に8MiBずつの配列を持たせて循環参照させます。それを100回繰り返します。

ログを取るためのスクリプトも用意しましょう。

#!/usr/bin/env bash

# 使い方:
#   ./log_mem.sh <PID>
#
# 例:
#   ./leaky_program &
#   pid=$!
#   ./log_mem.sh "$pid"

set -eu

pid="${1:-}"

if [ -z "$pid" ]; then
  echo "Usage: $0 <PID>" >&2
  exit 1
fi

log_file="mem_${pid}.csv"
echo "timestamp,rss_kb,vsz_kb" > "$log_file"

while kill -0 "$pid" 2>/dev/null; do
  ts=$(date +"%Y-%m-%dT%H:%M:%S")
  read _ rss vsz <<<"$(ps -o pid= -o rss= -o vsz= -p "$pid")"
  echo "$ts,$rss,$vsz" >> "$log_file"
  sleep 1
done

echo "process $pid finished; log saved to $log_file"

これを log_mem.sh という名前で保存し、計測してみます。

$cargo run --release &              
pid=$!
./log_mem.sh "$pid"

[1] 13333
   Compiling leak-drill v0.1.0 (/Users/todays_mitsui/private/leak-drill)
    Finished `release` profile [optimized] target(s) in 0.50s
     Running `target/release/leak-drill`
iter=10
iter=20
iter=30
iter=40
iter=50
iter=60
iter=70
iter=80
iter=90
iter=100
[1]  + done       cargo run --release
process 13333 finished; log saved to mem_13333.csv

結果はこうなりました。

timestamp rss_kb vsz_kb
2025-12-24T21:54:10 16448 410363040
2025-12-24T21:54:11 34000 410332416
2025-12-24T21:54:12 173632 410870016
2025-12-24T21:54:13 329312 411001088
2025-12-24T21:54:14 468640 411133184
2025-12-24T21:54:15 624352 411395328
2025-12-24T21:54:16 771904 411658496
2025-12-24T21:54:17 894880 412051712
2025-12-24T21:54:18 1058720 412051712
2025-12-24T21:54:19 1222560 412051712
2025-12-24T21:54:20 1370080 412313856
2025-12-24T21:54:21 1517536 412313856

RSS が順調に増えて、どんどんメモリを食っているのが見えますね!

 

Vec<T>.leak()

相互参照以外の方法も試しましょう。

Rust の可変長配列 Vec<T>.leak() というメソッドを実装しています。 .leak() は配列が確保しているヒープ領域を意図的に解放せず、 &'static mut [T] の形でスライスを取り出すことを許します。
シングルクォートから始まる 'aライフタイム 指定子 で、その中でも 'static は特別な「プロセスの全期間」を表すライフタイムです。つまり .leak() によって返されたスライスはプロセス実行中に解放されないことが約束されます。

use std::hint::black_box;

fn vec_leak(payload_bytes: usize) -> &'static [u8] {
    let v = vec![0u8; payload_bytes];
    black_box(&v);
    v.leak()
}

この関数は指定された長さの配列を作り、 .leak() を呼んでスライスを返します。
関数の中で定義された変数 v は、普通なら関数スコープの最後で所有権を失うか関数呼び出し元に所有権を move させるのですが。 .leak() によって意図的に所有権を放棄しているため関数スコープを抜けてもメモリが解放されません。

先ほどと同じく、この関数を100回呼んで計測しましょう。

use std::hint::black_box;
use std::time::Duration;

fn vec_leak(payload_bytes: usize) -> &'static [u8] {
    let v = vec![0u8; payload_bytes];
    black_box(&v);
    v.leak()
}

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let _slice = vec_leak(payload_bytes);
        }
        // ↑↑ ここで _slice のスコープが終わるが、_slice は借用なので所有権は失われない
        // もはやメモリは誰のものでもない

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

結果はこうなりました。

timestamp rss_kb vsz_kb
2025-12-24T22:20:06 16512 410494112
2025-12-24T22:20:07 50384 410462464
2025-12-24T22:20:08 107872 410855680
2025-12-24T22:20:10 165296 411117824
2025-12-24T22:20:11 239056 411248896
2025-12-24T22:20:12 312816 411379968
2025-12-24T22:20:13 394736 411379968
2025-12-24T22:20:14 476656 411379968
2025-12-24T22:20:15 542224 411511040
2025-12-24T22:20:16 607824 411773184
2025-12-24T22:20:17 689744 411773184

いいですね。

 

ManuallyDrop<T> とドロップ忘れ

std::mem::ManuallyDrop という構造体があります。変数の所有権が失われても自動でドロップ処理が呼ばれず、名前の通り手動でのドロップが必要になります。

let mut s = ManuallyDrop::new(String::new("Hoge"));

unsafe {
    ManuallyDrop::drop(&mut s);
}

忘れずに ManuallyDrop::drop() するのは実装者の責任です。これを忘れるとメモリは解放されずリークします。

手動ドロップをせずに100回呼んで観察してみましょう。

use std::hint::black_box;
use std::mem::ManuallyDrop;
use std::time::Duration;

fn main() {
    let payload_bytes: usize = 8 * 1024 * 1024; // 8MiB

    let mut i: u64 = 0;
    for _ in 0..100 {
        {
            let mut v = ManuallyDrop::new(vec![0u8; payload_bytes]);
            black_box(&mut v);

            // unsafe {
            //     ManuallyDrop::drop(&mut v);
            // }
        }
        // ↑↑ ここで v のスコープが終わる、v は ManuallyDrop なので所有権が失われても自動的にドロップされない
        // ManuallyDrop::drop() を呼び忘れているのでメモリは残り続ける

        i += 1;

        if i % 10 == 0 {
            println!("iter={}", i);
        }

        // 観察しやすいように少し待つ
        std::thread::sleep(Duration::from_millis(100));
    }
}

結果はこう。

timestamp rss_kb vsz_kb
2025-12-24T23:43:17 32 410060368
2025-12-24T23:43:18 1264 410593536
2025-12-24T23:43:19 66976 410864896
2025-12-24T23:43:20 140736 410995968
2025-12-24T23:43:21 222656 410995968
2025-12-24T23:43:22 288240 411127040
2025-12-24T23:43:23 362000 411258112
2025-12-24T23:43:24 435760 411389184
2025-12-24T23:43:25 517680 411389184
2025-12-24T23:43:26 591440 411520256
2025-12-24T23:43:27 629712 411651328

はい素晴らしい。

 

まとめ

今回はさまざまな方法でメモリリークするコードを動かして観察してみました。
Vec::leak(): &'static [T]ManuallyDrop<T> など、メモリを漏らすのにも型表現がついていてお行儀がいいですね。

 

 

私からは以上です。

 

参考

Special Thanks.