scheme in rust

scheme in rust

前回はrustのメモリー管理って亊で調べていたら、lispぽいやつが解説されてた。 ならば、それに特化したやつを探してみるかな。

(How to Write a (Lisp) Interpreter (in Python)) を、色々な言語で実装してみるってのが流行してる。で、rustでもやってみたいよねと言う人が、当然居るはずである。下記は、その二人に登場してもらったのを、しげしげと眺めている図であります。

目的はlispを実装する亊ではなくて、実装の考えかたをrust流にどう解釈されてるかを、理解する亊です。色々な人の理解をコードを通じて得ようという訳。

その弌

Risp (in (Rust) (Lisp))

risp >
(+ 1.2  3.4)
// 🔥 => 4.6

最初はグーじゃないけど、関数は、add,subしか無い簡易版。数値は浮動少数タイプもOK。いかん、いかん、最初の目的を忘れているね。

#[derive(Clone)]
enum RispExp {
  Symbol(String),
  Number(f64),
  List(Vec<RispExp>),
  Func(fn(&[RispExp]) -> Result<RispExp, RispErr>),
}

大事なのは、この列挙型だ。どんなLispの型を扱えるか宣言してる。そしてその型がrustでどう表現されるか、見てとれる。と、偉そうにしてるけど、前回の最後に出てた

enum List {
    Cons(i32, List),
    Nil,
}

これの意味が、解らないかった。rustがListってモジュールを提供してて、その中でConsとかNilてやつをサポートしてるって、盛大な誤解をしてたのさ。で、一生懸命にそんなモジュールを探してしまったぞ。

で、どうもおかしい。Listなんてそれっぽい名前は止めて、Hogeでいいだろうとか、Consも止めてPairでもいいかな? なんてやってみたのさ。バッチリOKだった。で、得た結論は、自前で勝手に定義していいんだって亊。まあ、 The Rust Programming Language 日本語版 とかを読めば、ちゃんと説明が有るんだけどね。説明の為の説明なんで、頭になかなか入らないのよ、と、弁解。

やっぱり、現場で疑問を持って、原典に当るのが、オイラーの性に合っているな。少々回り道っぽいけどね。

とか、思っていたら、タイミング良く Rustの構造体と列挙型を理解する が、紹介されてた。[解決!Python]クラスを定義するには なんかより、スッキリしてるな(贔屓してません?)。

#[derive(Clone)]
struct RispEnv {
  data: HashMap<String, RispExp>,
}

構造体が大事ってのは、経験から知っていたよ。新しい木箱に出会ったら、まず見ておくべき部分だ。昔の本に、まずはデータ構造を見ておけってのがあったけど、それにあたるね。

use std::io::{stdout, Write};

fn main() {
    let env = &mut default_env();
    loop {
        print!("risp> ");
        stdout().flush().unwrap();
        let expr = slurp_expr();
        match parse_eval(expr, env) {
            Ok(res) => println!("// 🔥 => {}", res),
            Err(e) => match e {
                RispErr::Reason(msg) => println!("// 🙀 => {}", msg),
 } } } }

プロンプトで改行しちゃうんで、しないようにflushを使ってみた。それから、閉括弧はLisp風にしてみたけど、似合わないか。

risp> (+ 3 5)
// 🔥 => 8
risp> (* 3 4)
// 🙀 => unexpected symbol k='*'

その弐

もう一人の方の解説 Building a Scheme Interpreter in Rust: Part 1 パートが進むにつれて複雑になっていく。けど、上で挙げた目的からすれば、 seanchen1991 / scheme-repl のパート1を見るだけで十分と思える。お好みで、 post_1 の所を切り換えれば、りっぱなschemeが出現するだろう。

この作者の方、名前から中国系っぽい、rustのコントリビューターらしいので、内容は確かだろう。そんな上から目線でいいのか? 我以外皆我師をモットーとするオイラーに、あるまじき発言だな。

scheme::parse_eval::h05b73bbca79fdc1c (input=..., env=0xcf7ed9d0)
    at src/main.rs:167
167       let evaluated = eval(&parsed, env)?;
(gdb) n
169       Ok(evaluated)
(gdb) p parsed
$3 = scheme::Expression
(gdb) p &parsed
$4 = (scheme::Expression *) 0xcf7ed848
(gdb) p *env
$6 = scheme::Env {
  operations: HashMap(size=2) = {
    ["-"] = scheme::Expression,
    ["+"] = scheme::Expression
  }
}

ガチガチに固められていて、中身を確認出来無いなあ。何か旨い手は無いものか? OpenBSDのqgdbが使いにくいなら、リナ系は? 帯に短し襷に長し。いらん情報は出てくるものの、ユーザーが欲しい情報は出ず。

こうなったら、昔ながらのprintでdebugだな。

fn eval(exp: &Expression, env: &mut Env) -> SResult<Expression> {
    println!("==> {}", exp);
    match exp {
     :

evalの入口で引数をprint。幸いな亊に、schemeで言うprint相当がインプリメントされてたんで、利用させて貰った。

evalの出口も表示させたかったけど、組み込みが面倒なので諦めた。ちゃんと出来てれば、schemeで言うtraceが実現出来たんだけどね。

scheme > (+ (+ 3 4) (- 10 2))
==> (+,(+,3,4),(-,10,2))
==> +
==> (+,3,4)
==> +
==> 3
==> 4
==> (-,10,2)
==> -
==> 10
==> 2
> 15

scmでtraceを味わっておく

> (trace +)
#<unspecified>
> (trace -)
#<unspecified>
> (+ (+ 3 4) (- 10 2))
call + 3 4
retn + 7
call - 10 2
retn - 8
call + 7 8
retn + 15
15

入門辺

そして、いよいよPeter Norvigさんのくびきから逃れて、自由な発想で、schemeを実装してる人達に出会います。探したのは、githubで、下記の検索をして、チョイスしました。

Implementation of Scheme interpreter in Rust

README.mdにあるように、ご本尊のconsが登場してます。そのあたりがどうなってるか、みたい。で、ソースのファイル名をみてたら、test.rsが有ったので。

sakae@deb:/tmp/Zefick-scheme/src$ cargo test
running 4 tests
test object::tests::test_format ... ok
test parser::tests::lexer_test ... ok
test parser::tests::parser_test ... ok
test tests::eval_test ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

真面目にtestを実施してるなら、良い仕様書だと思うぞ。

次は定石に則り、enum を点検。pubを付けてあるのが重要。

grep --color -nH --null -e enum *
functions.rs:9:pub enum Function {
object.rs:6:pub enum Number {
object.rs:12:pub enum Object {
parser.rs:4:enum Token {
service.rs:55:/// Since Rust doesn't support types for enum variants,
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Number {
    Float(f64),
    Integer(i32),
}

#[derive(PartialEq)]
pub enum Object { 4 implementations
    Nil,
    Boolean(bool),
    Symbol(String),
    String(String),
    Number(Number),
    Pair(Rc<Object>, Rc<Object>),
    Function(Function),
}

Pair型が出て来たな。consの枠組みになるんだな。Numberの中身が別の所で定義されてるenumって、名前がかぶっていて少々分かりづらい。本物のschemeは、有理数とか、複素数とか色々なるうちの限定採用なんで、それっぽい名前にして欲かったね。

list.rs

pub fn cons(obj: Rc<Object>) -> Result<Rc<Object>, String> {
    let (car, cdr) = expect_2_args(&obj, "cons")?;
    Ok(Rc::new(Object::Pair(car, cdr)))
}

もう、すっきりした関数だな。引数が2つある亊確認しつつ取出し。それをペアにしてるだけ。 巷のconsは、新しいセルをフリーリストから切り取ってきて、それにセットしてるけど、そんな面倒は無し。

/// The family of functions which names match with pattern `c[ad]{1,4}r`.
pub fn cadr(name: &str, obj: &Rc<Object>) -> Result<Rc<Object>, String> {
    let mut obj = expect_1_arg(&obj, name)?;
    for op in name[1..name.len() - 1].chars().rev() {
        let pair = check_pair(&obj)?;
        obj = Rc::clone(if op == 'a' { pair.0 } else { pair.1 });
    }
    return Ok(obj);
}

car,cdrが、scope.rsに登録されていなくて、どうなってるのと思ったら、まとめて処理してた。pair.0とかは、タプルのエレメントを指定する方法ね。 無限cxr を、shiroさんが、書いておられたな。対抗してるかと思ったら

(caaaaaaaaaaaaaadr 1)
Error: unbound variable 'caaaaaaaaaaaaaadr'

こんな風に怒られた。いい所まで来てるのに、もったいないな。制限をどこでかけてるの?

pub fn eval(obj: &Rc<Object>, scope: &Rc<Scope>) -> Result<Rc<Object>, String> {
    match obj.as_ref() {
        // resolve a symbol                                       
       Object::Symbol(s) => {
            if (s.starts_with("c") && s.ends_with("r"))
                && (s.len() >= 3 && s.len() <= 6)
                && (s[1..s.len() - 1].replace("a", "").replace("d", "")).is_empty()
            {
                Ok(Rc::new(Object::Function(Function::Dynamic(s.clone()))))
            } else {
                (scope.get(s)).ok_or_else(|| format!("unbound variable '{}'", s).to_string())
            }
        }

冒頭部分で、チャックしてた。長い cxr のサイズチェック、続いて、a か d が続いているかチェック。削除してって、残ってたら、それは、a,d 以外の文字が混入してたって判断。変に正規表現を持ち出さない所が偉いな。

ちょいと意地悪で、ドンドコとエリアを食い潰す奴を定義。consの中身は何でもいいんだけど、どこまで行っても終らないpiもどきを使ってみる。関数名は、永久保証の保険屋さんの名前から拝借。

(define (ever l) (ever (cons 3.1415926 l)))
()
(ever ())

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Abort trap (core dumped)

consエリアが尽きるより先に、スタックが無くなったしまったのかな。それならば、rustだけで、もどきを作ってみる。

enum List {
    Cons(f64, Rc<List>),
    Nil,
}

use std::rc::Rc;
use List::{Cons, Nil};

fn main() {
    let mut xx: Vec<List> = Vec::new();
    let mut cnt = 0;
    loop {
        xx.push(Cons(3.1415926, Rc::new(Nil)));
        cnt += 1;
        if cnt % 1_000_000 == 0 {
            println!("{}", cnt / 1_000_000);
        }
    }
}

schemeの意図した所とは微妙に違うけど、ドンドコとConsエリア(とVec)を消費するコードを書いてみた。1M毎に、進行状態を報告。

59
Killed

普通に実行すると、いきなり殺された。これだからリナって奴は(以下、自粛)。

57
58

Program terminated with signal SIGKILL, Killed.
The program no longer exists.
(gdb)

gdb上からだと、少しは防波堤の機能が働いている。それにしても、即死させられるの怖い。まて、OpenBSDの挙動を調べてみるか。

31
32
memory allocation of 32 bytes failed

/tmp: write failed, file system is full
Abort trap

ちゃんとメモリー不足って言ってきたね。どこかのリナとは違うんです!! それから、 write failedになってるのは、/tmpで実験してるんだけど、そこの容量不足の為、coreを収納出来無かった為だ。

topで挙動を確認してると、実メモリー不足になって、swapが動員され始め、そのうちに、どうにもメモリーのやりくりが出来無くなり、根をあげるって挙動がよく観察できた。

(gdb) bt
#0  scheme::lists::cadr::hefc2de276aa1c499 (name="car", obj=0xcf7d9844)
    at src/lists.rs:8
#1  0x15e87611 in scheme::functions::Function::call::h9c16ada508c9c978 (
    self=0x51018fcc, args=Rc(strong=1, weak=0) = {...}) at src/functions.rs:23
#2  0x15e81a0b in scheme::eval::eval::h8e563bf663ac05e6 (obj=0xcf7d9b24,
    scope=0xcf7da114) at src/eval.rs:148
#3  0x15e973d4 in scheme::main::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h7dca2da5a8c13acf (obj=...) at src/main.rs:58
  :

一応、お約束でバックトレース。モジュールに分割された作りになってる。

リファクタリング

上でちょいと文句を垂れた、Number(Number)て言う、紛らわしいやつ。リファクタリングと称して、Number(PSNumber)に変更してみた。新につけた接頭語PSは、パーシャル・スキームの積もり。

その結果、わんさかと7個もエラーが出現。例えば

error[E0432]: unresolved import `crate::object::Number`
 --> src/math.rs:1:20
  |
1 | use crate::object::Number::{Float, Integer};
  |                    ^^^^^^ could not find `Number` in `object`

こんなエラーだ。今迄、全く気にしてなかったけど、これ、オブジェクト一族の数字群って亊なのね。こういう風に、実践で勉強になるな。いじり回してみろってのは、この亊か。

最初は7個のエラーと思ってたけど、修整を始めると、エラーが増えた。上位でエラーが出ると、下部のチャックはしないと言うコンパイラーの方針なんだな。甘くみてると痛い目にあうぞ。 まあ、fly-checkが走っているおかげで、修整すべき部分は赤く表示されるから、機械的な作業ではあるけどね。

少し高度な奴

もう少し違ったものをって亊で、 scheme.rs を見付けてきた。

buildすると、結構警告が出て来る。

warning: `scheme-rs` (bin "scheme-rs") generated 10 warnings (7 duplicates)
    Finished dev [unoptimized + debuginfo] target(s) in 19.32s

10個あるって亊は、もっと厳格に査察したら、隠れヤバイヨ・ヤバイヨが炙り出されるに違いない。厳格版は、

ob$ cargo clippy
     :
error: written amount is not handled. Use `Write::write_all` instead
  --> src/repl.rs:13:9
   |
13 |         io::stdout().write(b"scheme.rs> ").unwrap();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[deny(clippy::unused_io_amount)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy
/master/index.html#unused_io_amount

warning: `scheme-rs` (lib) generated 84 warnings
error: could not compile `scheme-rs` due to 15 previous errors; 84 warnings emitted

こんな風に、clippyってオプションを使う。普通版だと、ちゃんとビルド出来ていたんだけど、コンパイルが失敗に終っている。全く融通の効かない査察官だ。

そういえば、ジェネリック医薬品の不都合な真実 なんて本を読み始めたのよ。アメリカで、口に入れる物全て(食料、飲料、医薬品)を監督する官庁であるFDAとジェネリック薬品メーカーの攻防をルポした本だ。

この本に出て来る査察官みたいだな。相手はインドの薬品メーカー、読み始めたばかりなんで、どう展開してくかは、今後の楽しみ。

先発メーカーのものと同等品ってのの重要なファクターとして、血液中に溶け出す有効成分の濃度が、既定内に入っている亊ってのがあるそうな。不純物の合有とかはどう既定してるのだろう? ジェネリック品を使う身としては、非常に興味があります。

まあ、先発メーカーは価格を自由に付けられる。ファイザーだとかモデルナとか、膨大な利益をあげているはず。ジェネリックは、まだか。そんなに言うなら、寿司のジェネリック品、回転寿司にでも行っとけ。

ああ、脱線しちゃったな。ちょいと査察してみる。

scheme.rs> (define (ever l) (ever (cons 3.1415926 l)))
scheme.rs> (ever '())

さっぱり尻尾を出しませんよ。どうなってんの? じっくり査察だな。 それより、他の奴も視野に入れてみるか。複数メーカーを選んでおくってのは、危険防止になりますからね。

本格派の奴

として、 vonuvoli-scheme なのを見付けた。

いさんでコンパイルしてみると

sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ cargo b
  :
error[E0554]: `#![feature]` may not be used on the stable release channel
  --> /home/sakae/.cargo/registry/src/github.com-1ecc6299db9ec823/blake2-rfc-0.2.18/src/lib.rs:19:31
   |
19 | #![cfg_attr(feature = "simd", feature(platform_intrinsics, repr_simd))]
   |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  :
For more information about this error, try `rustc --explain E0554`.
error: could not compile `blake2-rfc` due to 3 previous errors

あえなくエラー発生。手掛かりを表示させると、

sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ rustc --explain E0554
Feature attributes are only allowed on the nightly release channel. Stable or
beta compilers will not comply.

Erroneous code example:

```
#![feature(lang_items)] // error: `#![feature]` may not be used on the
                        //        stable release channel
```

If you need the feature, make sure to use a nightly release of the compiler
(but be warned that the feature may be removed or altered in the future).

夜なべ仕事で特徴を追加してるんで、最先端のrustを使え。だけど、それって気紛れかも知れないので保証の限りではない、とな。

果たして、こういう危い木箱を使う必要って有るの? はなはだ疑問。

sakae@pen:/tmp/rust-scheme/vonuvoli-scheme$ ls vendor/ | wc
    100     100    1019

使っている木箱を、cargo vendor でまとめてみて、その個数を数えてみた。木箱が増えるほど、こういう地獄を見る機会が多くなる。前にもあったよな、psutilで、木箱がちゃんとコンパイル出来無い事例が。

その対策として、Cargo.lockが用意されてて、木箱のバージョンをロックしちゃうってのがあった。けど、今回のように環境の差異とか、後追いでやろうとしたら、リポジトリィーが変ってしまってて駄目だったってのには対抗出来無い。OSSだから根本解決は無理。

build vs. check

上の長大のしかも失敗するやつに、懲りた。初めてのやつは素早く合否を判定したいぞ。 そこで、buildした場合とcheckした場合で、どれぐらいの差があるか、確認してみる。

ob$ time cargo b
     :
    Finished dev [unoptimized + debuginfo] target(s) in 28.90s
    0m28.93s real     0m39.79s user     0m43.58s system
ob$ time cargo c
     :
    Finished dev [unoptimized + debuginfo] target(s) in 26.93s
    0m26.95s real     0m30.34s user     0m39.23s system

4CPUのOpenBSDなんだけど、何も指定しなくても複数のCPUが動いているみたいだな。

カタログ

皆さん、木箱を色々活用してるけど、どうやって星の数程有る中から見つけ出しているの? https://crates.io/ が、一択?

まあ、それでもいいんだけど、別のカタログもみたいぞ。ハードをやってた時代は、石のカタログ(トランジスタ)と言えば、CQ出版のカタログのお世話になったし、ICと言えば、テキサス・インスツルメントのSNXXシリーズの規格表のおせわになったものだ。

その乗りで、部品を探したい。そんなんで見付けてしまったのが、 lib catlog だ。カテゴリー分けがしてあるので、割と探し易い。これの嬉しいのが、依存関係も表示してくれる亊。依存が少ければ少ない程安全だ。それから、作者が途中で放り出していないかも、そこはかとなく分る。

もう一点、 Awesome Rust 素敵なRustってのも素敵だ。便利に使いたい。

lisp's parts