$30 off During Our Annual Pro Sale. View Details »

Rust言語で作るインタプリタ / Write An Interpreter In Rust

Rust言語で作るインタプリタ / Write An Interpreter In Rust

レトリバセミナー 2019/08/21
Movie: https://www.youtube.com/watch?v=zdq1lUo7b-I
Source Code: https://github.com/kei-s/waiir

Kei Shiratsuchi
PRO

August 21, 2019
Tweet

More Decks by Kei Shiratsuchi

Other Decks in Technology

Transcript

  1. Rust⾔語で作るインタプリタ
    株式会社レトリバ
    ⽩⼟ 慧
    © 2017 Retrieva, Inc.

    View Slide

  2. ⾃⼰紹介
    • ⽩⼟ 慧(シラツチ ケイ)
    • 略歴
    • 2008-2011 サイジニア株式会社
    • 2012-2016 株式会社オーマイグラス
    • 2016/4- PFI → 株式会社レトリバ
    • Ruby on Rails / JavaScript
    • 趣味:通勤中の読書・映画
    • 「マイホームヒーロー 」⼭川直輝原作、朝基まさし作画
    • 「⼤統領失踪」ビル・クリントン、ジェイムズ・パタースン
    • 「三体」劉慈欣
    • 「Fake」森達也「淋しいのはアンタだけじゃない」吉本浩⼆
    © 2017 Retrieva, Inc. 2

    View Slide

  3. 概要
    • 『Go⾔語で作るインタプリタ』
    • https://www.oreilly.co.jp/books/9784873118222/
    • を、Rust で再現する
    • https://github.com/kei-s/waiir
    • Write An Interpreter In Rust
    • 本編まで完了
    • 付録のマクロシステムは未着⼿
    • コミットログに⽇記を書いた
    © 2017 Retrieva, Inc. 3

    View Slide

  4. アジェンダ
    © 2017 Retrieva, Inc. 4

    View Slide

  5. アジェンダ
    • 『Go⾔語で作るインタプリタ』
    • Rust
    • 今回作る⾔語 “Monkey” の紹介
    • インタプリタの流れ
    • 字句解析
    • 構⽂解析
    • 評価
    • まとめ
    © 2017 Retrieva, Inc. 5

    View Slide

  6. Go⾔語で作るインタプリタ
    © 2017 Retrieva, Inc. 6

    View Slide

  7. Go⾔語で作るインタプリタ
    © 2017 Retrieva, Inc. 7

    View Slide

  8. Go⾔語で作るインタプリタ
    • 動くものを⼩さく、テスト駆動で作る
    © 2017 Retrieva, Inc. 8

    View Slide

  9. Rust
    前々回のセミナー資料から抜粋
    最近個⼈的に気になるプログラミング⾔語おさらい Ruby, Python, Go, Rust,
    Julia
    © 2017 Retrieva, Inc. 9

    View Slide

  10. Rust
    • Mozilla が開発を主導
    • ベターCを⽬指すっぽい(分野としてC++と競合しそう)
    • 安全性、速度、並⾏性の3つのゴールにフォーカス
    • ガーベジコレクタなしにこれらのゴールを実現
    • 他の⾔語への埋め込み、要求された空間や時間内での動作、 デバイス
    ドライバやオペレーティングシステムのような低レベルなコードを得
    意とする
    • 全てのデータ競合を排除しつつも実⾏時オーバーヘッドのないコンパ
    イル時の安全性検査を多数持つ
    © 2017 Retrieva, Inc. 10

    View Slide

  11. Rust の特徴:所有権と借⽤
    • 変数の「所有権」をやり取りすることで、メモリ安全性を確保する
    • 違反する場合、コンパイル時にエラーにする
    • これにより、マルチスレッドでのデータ競合などを未然に防ぐ
    • 「しかし、このシステムはあるコストを持ちます。それは学習曲線です。
    多くの新しいRustのユーザは「借⽤チェッカとの戦い」と好んで呼ばれる
    ものを経験します。
    … … …
    しかし、よいニュースがあります。… … …⼀度彼らが所有権システムの
    ルールとともにしばらく仕事をすれば、彼らが借⽤チェッカと戦うことは
    少なくなっていくということです。」
    © 2017 Retrieva, Inc. 11

    View Slide

  12. Rust の特徴:所有権と借⽤
    • 変数の「所有権」をやり取りすることで、メモリ安全性を確保する
    • 違反する場合、コンパイル時にエラーにする
    • これにより、マルチスレッドでのデータ競合などを未然に防ぐ
    • 「しかし、このシステムはあるコストを持ちます。それは学習曲線です。
    多くの新しいRustのユーザは「借⽤チェッカとの戦い」と好んで呼ばれる
    ものを経験します。
    … … …
    しかし、よいニュースがあります。… … …⼀度彼らが所有権システムの
    ルールとともにしばらく仕事をすれば、彼らが借⽤チェッカと戦うことは
    少なくなっていくということです。」
    © 2017 Retrieva, Inc. 12

    View Slide

  13. Rust:所有権
    • リソース v に対する所有権は「1つ」しかない
    • 下記は println! のところで「コンパイル時に」エラーになる
    • error[E0382]: use of moved value: `v`
    © 2017 Retrieva, Inc. 13
    fn main() {
    let v = vec![1, 2, 3];
    let v2 = v;
    println!("v[0] is: {}", v[0]);
    }
    fn take(v: Vec) {
    println!(”v{}", v[0]);
    }
    fn main() {
    let v = vec![1, 2, 3];
    take(v);
    println!("v[0] is: {}", v[0]);
    }

    View Slide

  14. Rust:参照と借⽤
    • v を渡す代わりに、&v を渡す。引数にも &Vec を使う。
    • &T, &mut T は参照。所有権を「借⽤」する。
    © 2017 Retrieva, Inc. 14
    fn take(v: &Vec) {
    println!("{}", v[0]);
    }
    fn main() {
    let v = vec![1, 2, 3];
    take(&v);
    println!("v[0] is: {}", v[0]);
    }
    fn plus(y: &mut i32) {
    *y += 1;
    }
    fn main() {
    let mut x = 5;
    plus(&mut x);
    println!("{}", x);
    }

    View Slide

  15. P.11「はじめに」
    © 2017 Retrieva, Inc. 15

    View Slide

  16. P.11「はじめに」
    © 2017 Retrieva, Inc. 16

    View Slide

  17. 謝辞
    • Rust で実装した偉⼤なる先⼈
    • kogai/monkey
    • tsuyoshiwada/rs-monkey-lang
    • avocado-shrimp/Monkey
    • めちゃめちゃ参考にさせていただきました
    © 2017 Retrieva, Inc. 17

    View Slide

  18. “Monkey”
    © 2017 Retrieva, Inc. 18

    View Slide

  19. 今回作る⾔語 “Monkey”
    © 2017 Retrieva, Inc. 19
    let age = 1;
    let name = "Monkey";
    let result = 10 * (20 / 2);
    let myArray = [1, 2, 3, 4, 5];
    let myHash = {"name": ”Hi", "age": 28};
    myArray[0] // => 1
    myHash["name"] // => ”Hi"
    let add = fn(a, b) { return a + b; };
    let add = fn(a, b) { a + b; };
    add(1, 2);
    • C⾔語⾵の構⽂
    • 変数束縛
    • 整数と真偽値
    • 算術式
    • ⽂字列データ型
    • 配列データ型
    • ハッシュデータ型
    • 関数

    View Slide

  20. 今回作る⾔語 “Monkey”
    © 2017 Retrieva, Inc. 20
    let twice = fn(f, x) { return f(f(x)); };
    let addTwo = fn(x) { return x + 2; };
    twice(addTwo, 2); // => 6
    let fibonacci = fn(x) {
    if (x == 0) {
    0
    } else {
    if (x == 1) {
    1
    } else {
    fibonacci(x - 1) + fibonacci(x - 2);
    }
    }
    };
    • クロージャ
    • 関数の再帰呼び出し
    • 第⼀級の⾼階関数

    View Slide

  21. デモ
    • cargo build --release して target/release/waiir で REPL 起動
    • もしくは cargo run
    • 複数⾏を受け付けられないので注意
    © 2017 Retrieva, Inc. 21

    View Slide

  22. インタプリタの流れ
    © 2017 Retrieva, Inc. 22

    View Slide

  23. インタプリタの流れ
    1. ソースコードが⽂字列として⼊⼒される
    2. ⽂字列をトークン列に変換する(字句解析)
    3. トークン列を抽象構⽂⽊に変換する(構⽂解析)
    4. 抽象構⽂⽊を辿り実⾏する(評価)
    © 2017 Retrieva, Inc. 23
    let x = 5 + 5;
    [
    LET,
    IDENTIFIER(“x”),
    EQUAL_SIGN,
    INTEGER(5),
    PLUS_SIGN,
    INTEGER(5),
    SEMICOLON,
    ]
    let ⽂
    x 中置演算⼦式
    5 + 5
    x //=> 10

    View Slide

  24. 字句解析
    © 2017 Retrieva, Inc. 24

    View Slide

  25. 字句解析
    • トークンを定義する
    • Token { type: token_type, literal: string} の構造体
    • Token { type: “Ident”, literal: “x”}
    • token_type
    • Ident // x, y, foo, bar
    • Int // 0, 1, 2, 100, 12345
    • String // “aaa”, “Hello”
    • Assign // =
    • Lt // <
    • Eq // ==
    • Comma // ,
    • LParen // (
    • Rbrace // }
    • Function, Let, True, False, If, Else, Return
    © 2017 Retrieva, Inc. 25

    View Slide

  26. 字句解析
    • ソースコードの⽂字列を1⽂字ずつ⼿前から順に読んでいき、
    トークン列を作成する
    • 記号 => 対応する記号 (+, -, =, ==, !=, …) のトークン
    • “ => “ で終わるまでの⽂字列の String トークン
    • ⽂字列 => 予約語 (let, return, fn, …) ならそのトークン
    そうでなければ Ident トークン
    • 数字 => 数値トークン
    • Monkey では整数のみ扱い、⼩数、8進数、16進数などは使えないとする
    • 空⽩、タブ⽂字、改⾏ => 読み⾶ばす
    © 2017 Retrieva, Inc. 26

    View Slide

  27. 字句解析 lexer.rs の実装解説
    • ⽂字を取り出す際、Go では⽂字列 input のインデックスアク
    セスで良いが、
    • ch = input[position]
    • Rust ではインデックスアクセスだとUTF-8の⽂字ではなく
    Byte列が返る。ため、input.chars() で UTF-8 の⽂字のイテ
    レータを利⽤する
    • このイテレータはライフタイム付きで利⽤する⽅もライフタイムを管
    理する必要があってしんどいので⼀度ベクタに展開している [解説略!]
    © 2017 Retrieva, Inc. 27

    View Slide

  28. 字句解析 lexer.rs の実装解説
    • Go ではプログラムの終端を EOF トークンで管理している
    • Lexer ⾃体を Iterator として実装したので、l.next() を呼び、終
    端では None を返すことにする
    • impl Iterator for Lexer
    • trait という仕組み
    • Ruby でいう include Enumerable 的な仕組みと、Java とかのインターフェース
    的な仕組み
    • Smalltalk などの元々あったトレイトという概念とは結構違いがあるとのこと
    © 2017 Retrieva, Inc. 28

    View Slide

  29. 構⽂解析
    © 2017 Retrieva, Inc. 29

    View Slide

  30. 構⽂解析
    • yacc, bison などのパーサジェネレータを使わず、⼀から作る
    • BNF 書かない
    • 再帰下降構⽂解析という⼿法の、Pratt構⽂解析を実装
    • 昔から存在していたが、近年 JSLint の実装に使われたり再発⾒された
    © 2017 Retrieva, Inc. 30

    View Slide

  31. 構⽂解析
    • の前に、「⽂」と「式」のおさらい
    • こんがらがるよね…
    • 戻り値がないのが⽂ (Statement)
    • let x = 5;
    • return x;
    • 戻り値があるのが式 (Expression)
    • let x = foo;とか return foo; の foo に⼊れられるのが式
    • x + 5
    • fn(a, b) { a + b }
    • if(flag) { 1 } else { 2 }
    • Monkey では if は⽂ではなく式 (モダンな気がする) (JavaScript だと⽂)
    • let result = if(flag) { 1 } else { 2 };
    © 2017 Retrieva, Inc. 31

    View Slide

  32. 構⽂解析
    • 構⽂解析のキモ:演算⼦の優先順位
    • 5 + 5 * 5 //=> (5 + (5 * 5))
    • (5 + 5) * 5 //=> ((5 + 5) * 5)
    • - 5 * 5 //=> ((- 5) * 5)
    • true == !false //=> (true == (!false))
    • !(true == !false) // => (!(true == (!false)))
    • Pratt 構⽂解析は、前置演算⼦と中置演算⼦、演算⼦の優先順位を駆
    使して再帰的に構⽂解析する
    • array[0] や add(1,2) とかも、 [ や ( を中置演算⼦として扱う
    • Monkey では後置演算⼦(foo++ とか)は存在しない
    © 2017 Retrieva, Inc. 32

    View Slide

  33. 構⽂解析
    • 抽象構⽂⽊(Abstract Syntax Tree)を定義する
    • ⽂ Statement
    • LetStatement { name: Identifer, value: Expression }
    • let name = value;
    • ReturnStatement { return_value: Expression }
    • return return_value;
    • …
    • 式 Expression
    • Identifier { value: String }
    • Integer { value: Int64 }
    • PrefixExpression { operator: String, right: Expression } // -5, !flag
    • InfixExpression { left: Expression, operator: String, right: Expression } // 5 + 5, a * b, (a + b) * (c - d)
    • …
    • ルート Program
    • Program { statements: Statement[] }
    © 2017 Retrieva, Inc. 33

    View Slide

  34. 構⽂解析
    • let x = -(5 + 5) * 5;
    © 2017 Retrieva, Inc.
    34
    Program
    statements
    Identifier
    value: “x”
    IntegerLiteral
    value: 5
    LetStatement
    name
    value
    InfixExpression
    left
    operator: “*”
    right
    PrefixExpression
    operator: “-”
    right
    InfixExpression
    left
    operator: “+”
    right
    IntegerLiteral
    value: 5
    IntegerLiteral
    value: 5

    View Slide

  35. 構⽂解析の擬似コード
    • トークン列を⼿前から順に読んでいく
    • トークンに対応する関数でASTのノードを作る
    © 2017 Retrieva, Inc. 35
    function parseProgram() {
    program = newProgramASTNode()
    advanceTokens()
    for (currentToken() != EOF_TOKEN) {
    statement = null
    if (currentToken() == LET_TOKEN) {
    statement = parseLetStatement()
    } else if (currentToken() == RETURN_TOKEN) {
    statement = parseReturnStatement()
    }
    if (statement != null) {
    program.Statements.push(statement)
    }
    advanceTokens()
    }
    return program
    }
    function parseLetStatement() {
    advanceTokens()
    identifier = parseIdentifier()
    advanceTokens()
    if currentToken() != EQUAL_TOKEN {
    parseError("no equal sign!")
    return null
    }
    advanceTokens()
    value = parseExpression(LOWEST)
    variableStatement = newVariableStatementASTNode()
    variableStatement.identifier = identifier
    variableStatement.value = value
    return variableStatement
    }
    let x = 5 + 5 * 5;

    View Slide

  36. 構⽂解析の擬似コード
    © 2017 Retrieva, Inc. 36
    enum Precedence (
    LOWEST
    EQUALS // ==
    LESSGREATER // > or <
    SUM // +
    PRODUCT // *
    PREFIX // -X or !X
    CALL // myFunction(X)
    INDEX // array[index]
    )
    prefixParseFuncs = {
    IDENT: parseIdentifier,
    INT: parseIntegerLiteral,
    STRING: parseStringLiteral,
    BANG: parsePrefixExpression,
    // [...]
    }
    infixParseFuncs = {
    PLUS: parseInfixExpression,
    MINUS: parseInfixExpression,
    LPAREN: parseCallExpression,
    // [...]
    }
    function parseExpression(precedence) {
    token = currentToken();
    prefix_func = prefixParseFuncs.get(token.type);
    left = prefix_func();
    while (precedence < peekPrecedence()) {
    token = currentToken();
    infix_func = infixParseFuncs.get(token.type);
    left = infix_func(left);
    }
    return left;
    }
    function parseInfixExpression(left) {
    operatorExpression = newOperatorExpression()
    operatorExpression.left = left
    operatorExpression.operator = currentToken()
    precedence = currentPrecedence()
    operatorExpression.right = parseExpression(precedence)
    return operatorExpression
    }

    View Slide

  37. 構⽂解析の擬似コード
    © 2017 Retrieva, Inc. 37
    enum Precedence (
    LOWEST
    EQUALS // ==
    LESSGREATER // > or <
    SUM // +
    PRODUCT // *
    PREFIX // -X or !X
    CALL // myFunction(X)
    INDEX // array[index]
    )
    prefixParseFuncs = {
    IDENT: parseIdentifier,
    INT: parseIntegerLiteral,
    STRING: parseStringLiteral,
    BANG: parsePrefixExpression,
    // [...]
    }
    infixParseFuncs = {
    PLUS: parseInfixExpression,
    MINUS: parseInfixExpression,
    LPAREN: parseCallExpression,
    // [...]
    }
    function parseExpression(precedence) {
    token = currentToken();
    prefix_func = prefixParseFuncs.get(token.type);
    left = prefix_func();
    while (precedence < peekPrecedence()) {
    token = currentToken();
    infix_func = infixParseFuncs.get(token.type);
    left = infix_func(left);
    }
    return left;
    }
    function parseInfixExpression(left) {
    operatorExpression = newOperatorExpression()
    operatorExpression.left = left
    operatorExpression.operator = currentToken()
    precedence = currentPrecedence()
    operatorExpression.right = parseExpression(precedence)
    return operatorExpression
    }
    演算⼦の優先順位を定義する
    SUM < PRODUCT

    View Slide

  38. 構⽂解析の擬似コード
    © 2017 Retrieva, Inc. 38
    enum Precedence (
    LOWEST
    EQUALS // ==
    LESSGREATER // > or <
    SUM // +
    PRODUCT // *
    PREFIX // -X or !X
    CALL // myFunction(X)
    INDEX // array[index]
    )
    prefixParseFuncs = {
    IDENT: parseIdentifier,
    INT: parseIntegerLiteral,
    STRING: parseStringLiteral,
    BANG: parsePrefixExpression,
    // [...]
    }
    infixParseFuncs = {
    PLUS: parseInfixExpression,
    MINUS: parseInfixExpression,
    LPAREN: parseCallExpression,
    // [...]
    }
    function parseExpression(precedence) {
    token = currentToken();
    prefix_func = prefixParseFuncs.get(token.type);
    left = prefix_func();
    while (precedence < peekPrecedence()) {
    token = currentToken();
    infix_func = infixParseFuncs.get(token.type);
    left = infix_func(left);
    }
    return left;
    }
    function parseInfixExpression(left) {
    operatorExpression = newOperatorExpression()
    operatorExpression.left = left
    operatorExpression.operator = currentToken()
    precedence = currentPrecedence()
    operatorExpression.right = parseExpression(precedence)
    return operatorExpression
    }
    対応する関数を実⾏する
    対応する関数を実⾏する
    +
    -
    (
    トークンごとに
    前置演算⼦、中置演算⼦に来た時
    パースする関数を定義する

    View Slide

  39. 構⽂解析の擬似コード
    © 2017 Retrieva, Inc. 39
    enum Precedence (
    LOWEST
    EQUALS // ==
    LESSGREATER // > or <
    SUM // +
    PRODUCT // *
    PREFIX // -X or !X
    CALL // myFunction(X)
    INDEX // array[index]
    )
    prefixParseFuncs = {
    IDENT: parseIdentifier,
    INT: parseIntegerLiteral,
    STRING: parseStringLiteral,
    BANG: parsePrefixExpression,
    // [...]
    }
    infixParseFuncs = {
    PLUS: parseInfixExpression,
    MINUS: parseInfixExpression,
    LPAREN: parseCallExpression,
    // [...]
    }
    function parseExpression(precedence) {
    token = currentToken();
    prefix_func = prefixParseFuncs.get(token.type);
    left = prefix_func();
    while (precedence < peekPrecedence()) {
    token = currentToken();
    infix_func = infixParseFuncs.get(token.type);
    left = infix_func(left);
    }
    return left;
    }
    function parseInfixExpression(left) {
    operatorExpression = newOperatorExpression()
    operatorExpression.left = left
    operatorExpression.operator = currentToken()
    precedence = currentPrecedence()
    operatorExpression.right = parseExpression(precedence)
    return operatorExpression
    }
    現在の優先順位を渡し
    その優先順位の式をパースする
    中置演算⼦では
    渡ってきた left をアサインする

    View Slide

  40. 構⽂解析の擬似コード
    © 2017 Retrieva, Inc. 40
    enum Precedence (
    LOWEST
    EQUALS // ==
    LESSGREATER // > or <
    SUM // +
    PRODUCT // *
    PREFIX // -X or !X
    CALL // myFunction(X)
    INDEX // array[index]
    )
    prefixParseFuncs = {
    IDENT: parseIdentifier,
    INT: parseIntegerLiteral,
    STRING: parseStringLiteral,
    BANG: parsePrefixExpression,
    // [...]
    }
    infixParseFuncs = {
    PLUS: parseInfixExpression,
    MINUS: parseInfixExpression,
    LPAREN: parseCallExpression,
    // [...]
    }
    function parseExpression(precedence) {
    token = currentToken();
    prefix_func = prefixParseFuncs.get(token.type);
    left = prefix_func();
    while (precedence < peekPrecedence()) {
    token = currentToken();
    infix_func = infixParseFuncs.get(token.type);
    left = infix_func(left);
    }
    return left;
    }
    function parseInfixExpression(left) {
    operatorExpression = newOperatorExpression()
    operatorExpression.left = left
    operatorExpression.operator = currentToken()
    precedence = currentPrecedence()
    operatorExpression.right = parseExpression(precedence)
    return operatorExpression
    }
    中置演算⼦の処理にて
    今パースしている式の優先順位が
    次のトークンの優先順位より⾼くなるま
    で、
    この関数が返す式を更新する

    View Slide

  41. 構⽂解析 parser.rs の実装解説
    • 本では途中、未実装のトークンが来た場合に nil を返す
    • Rust には NULL がないため無視するとコンパイルできない。
    その時点でもエラー処理を考えないといけない
    • エラー処理は Result 型を使う
    • prefixParseFuncs を作らず、都度 match して分岐させる
    • HashMap に渡すものの型もしっかり定義しないといけない
    • (作り終わった今ならできそうな気もするが…)
    © 2017 Retrieva, Inc. 41

    View Slide

  42. 構⽂解析 parser.rs の実装解説
    • 本ではInfixExpression は Expression インターフェースを実装した構造体
    • Go ではインターフェースから実体を取れる
    © 2017 Retrieva, Inc. 42
    func parse(exp ast.Expression) {
    opExp, ok := exp.(*ast.InfixExpression)
    // opExp.operator にアクセスできる
    struct InfixExpression {
    operator: String
    }
    trait Expression {…}
    impl Expression for InfixExpression {…}
    impl Expression for PrefixExpression {…}
    fn parse(exp Expression) {
    // exp は InfixExpression ではないので、
    // exp.operator にアクセスできない
    • Rust では trait から実体を取れない

    View Slide

  43. 構⽂解析 parser.rs の実装解説
    • Enum の列挙⼦にデータを格納
    • パターンマッチで中⾝にアクセスできる
    © 2017 Retrieva, Inc. 43
    enum Expression {
    Infix(InfixExpression),
    Prefix(PrefixExpression),
    }
    exp = Expression::Infix(InfixExpression { operator: "+" })
    fn parse(exp Expression) {
    match exp {
    Expression::Infix(infix) => infix.operator,
    // exp は InfixExpression なのでアクセスできる
    Expression::Prefix(prefix) => ...,
    }
    }

    View Slide

  44. 評価
    © 2017 Retrieva, Inc. 44

    View Slide

  45. 評価
    • 構築した抽象構⽂⽊を評価する
    • インタプリタとコンパイラの境界は実は曖昧
    • インタプリタでも、 AST 書き換えたり、中間表現(IR)へ変換したり、
    JITコンパイラで⼀部をコンパイルしたり
    • Monkey は、tree-walking インタプリタ
    • Ruby 1.8 まで採⽤されていた⽅式
    © 2017 Retrieva, Inc. 45

    View Slide

  46. tree-walking インタプリタ(擬似コード)
    • ノードを引数にとる eval 関数を再帰的に呼び出す
    • ノードに記録された値を使って評価時のオブジェクトを返す
    © 2017 Retrieva, Inc. 46
    function eval(astNode) {
    if (astNode is integerliteral) {
    return Integer{ value: astNode.integerValue }
    } else if (astNode is booleanLiteral) {
    return Bool{ value: astNode.booleanValue }
    } else if (astNode is infixExpression) {
    leftEvaluated = eval(astNode.Left)
    rightEvaluated = eval(astNode.Right)
    if astNode.Operator == "+" {
    return Integer{ value: leftEvaluated.value + rightEvaluated.value }
    } else if ast.Operator == "-" {
    return Integer{ value: leftEvaluated.value – rightEvaluated.value }
    }
    }
    }

    View Slide

  47. AST ノードの評価の例(Go)
    • if (flag) { puts(1) } else { puts(2) }
    • condition に合わない⽅のノードは評価してはいけない
    © 2017 Retrieva, Inc. 47
    func evalIfExpression(ie *ast.IfExpression) object.Object {
    condition := Eval(ie.Condition)
    if isTruthy(condition) {
    return Eval(ie.Consequence)
    } else if ie.Alternative != nil {
    return Eval(ie.Alternative)
    } else {
    return NULL
    }
    }

    View Slide

  48. 束縛と環境(擬似コード)
    • 束縛
    • let x = 5 * 5;
    • この⽂を評価した後、 x を評価すると 25 を返すようにする
    • 環境
    • ⽂字列とオブジェクトの紐付けを管理するハッシュマップ
    • eval の引数にして引き回す
    © 2017 Retrieva, Inc. 48
    struct Envirionment {
    store: Map
    }
    function Envirionemnt.get(name) {
    store,get(name)
    }
    function Envirionemnt.set(name, val) {
    store[name] = val
    }
    function eval(astNode, env) {
    leftEvaluated = eval(astNode.Left, env)

    }

    View Slide

  49. 束縛と環境(擬似コード)
    • LetStatement の評価時に、名前と値を環境に保存する
    • Identifier の評価時に、環境から値を取り出す
    © 2017 Retrieva, Inc. 49
    function eval(node, env) {
    switch node.type {
    case LetStatement:
    val = eval(node.value, env)
    env.set(node.name.value, val)
    case Identifier:
    val = env.get(node.value)
    if val == null {
    return error("Identifier not found")
    }
    return val

    View Slide

  50. 関数定義(擬似コード)
    • let add = fn(x, y) { x + y }
    • Monkey では fn add(x, y) は無いので、Function オブジェクトを返し
    て add に束縛させる
    • 引数、中⾝と⼀緒に、その時の環境も保存しておく
    © 2017 Retrieva, Inc. 50
    function eval(node, env) {
    switch node.type {
    case FunctionLiteral:
    params = node.parameters
    body = node.body
    return Function{ parmeters: params, body: body, env: env }
    }
    }

    View Slide

  51. 関数呼び出し
    • の前に、期待する動作
    • 何も考えず引数を今の環境に上書きすると、同じ i なので関数を抜けたあとも 10 のままになる
    • 関数の引数は、元の環境を保持したまま新しい環境を作成する必要がある
    • 関数内での識別⼦の呼び出しは、新しい環境から探し、⾒つからない場合は元の環境に
    フォールバックして探す
    • => クロージャ!!!
    © 2017 Retrieva, Inc. 51
    let i = 5;
    let printNum = fn(i) {
    puts(i);
    };
    printNum(10); //=> 10
    puts(i); //=> 5

    View Slide

  52. 関数呼び出し(擬似コード)
    © 2017 Retrieva, Inc. 52
    function eval(node, env) {
    switch node.type {
    case CallExpression:
    func = eval(node.func, env)
    args = []
    for e in node.arguments {
    args.push(eval(e, env))
    }
    return applyFunction(func, args)
    }
    }
    struct Environment {
    store: Map
    outer: Environment
    }
    function newEnclousedEnvironment(outer) {
    env = Environment.new
    env.outer = outer
    return env
    }
    function Environment.get(mame) {
    obj = store,get(name)
    if obj == null && outer != null {
    return outer.get(name)
    }
    return obj
    }
    function applyFunction(func, args) {
    extendedEnv = newEnclousedEnvironment(fn.env)
    for i, param in fn.parameters {
    extendedEnv.set(param.value, args[i])
    }
    evaluated = eval(func.body, extendedEnv)
    return evaluated
    }

    View Slide

  53. struct Environment {
    store: Map
    outer: Environment
    }
    function newEnclousedEnvironment(outer) {
    env = Environment.new
    env.outer = outer
    return env
    }
    function Environment.get(mame) {
    obj = store,get(name)
    if obj == null && outer != null {
    return outer.get(name)
    }
    return obj
    }
    関数呼び出し(擬似コード)
    © 2017 Retrieva, Inc. 53
    function eval(node, env) {
    switch node.type {
    case CallExpression:
    func = eval(node.func, env)
    args = []
    for e in node.arguments {
    args.push(eval(e, env))
    }
    return applyFunction(func, args)
    }
    }
    function applyFunction(func, args) {
    extendedEnv = newEnclousedEnvironment(fn.env)
    for i, param in fn.parameters {
    extendedEnv.set(param.value, args[i])
    }
    evaluated = eval(func.body, extendedEnv)
    return evaluated
    }
    外側の環境を保持するフィールドを追加
    外側の環境を保持した環境を返す
    現在の環境で値が⾒つからず
    外側の環境があるとき
    外側の環境から探してくる

    View Slide

  54. struct Environment {
    store: Map
    outer: Environment
    }
    function newEnclousedEnvironment(outer) {
    env = Environment.new
    env.outer = outer
    return env
    }
    function Environment.get(mame) {
    obj = store,get(name)
    if obj == null && outer != null {
    return outer.get(name)
    }
    return obj
    }
    関数呼び出し(擬似コード)
    © 2017 Retrieva, Inc. 54
    function eval(node, env) {
    switch node.type {
    case CallExpression:
    func = eval(node.func, env)
    args = []
    for e in node.arguments {
    args.push(eval(e, env))
    }
    return applyFunction(func, args)
    }
    }
    function applyFunction(func, args) {
    extendedEnv = newEnclousedEnvironment(fn.env)
    for i, param in fn.parameters {
    extendedEnv.set(param.value, args[i])
    }
    evaluated = eval(func.body, extendedEnv)
    return evaluated
    }
    与えられた引数の式を
    それぞれ評価しておく
    引数の名前で環境を作成
    外側の環境には、
    関数定義時の環境 fn.env を渡す
    作成した環境で関数の中⾝を評価

    View Slide

  55. 関数定義と関数呼び出し
    • これでクロージャが実現する!
    • ので、以下のコード(再帰、⾼階関数)が動く
    © 2017 Retrieva, Inc. 55
    let fibonacci = fn(x) {
    if (x == 0) {
    0
    } else {
    if (x == 1) {
    1
    } else {
    fibonacci(x - 1) + fibonacci(x - 2);
    }
    }
    };
    let twice = fn(f, x) { return f(f(x)); };
    let addTwo = fn(x) { return x + 2; };
    twice(addTwo, 2); // => 6

    View Slide

  56. 評価 evaluator.rs の実装解説
    • Go では eval(ast.Node) のように、全ての AST ノードが Node イン
    ターフェースを持つようにしている
    • Rust では、trait Eval を導⼊して、全てのノードに node.eval を⽣
    やすようにした
    • この辺の構成は今ならもっと綺麗にできるかもしれない…
    • ↑ の定義で同じようなことを書く場⾯が増えたので、マクロを導⼊
    した
    • 使ってみたかっただけ
    • 典型的な「早すぎる最適化」だったので、無駄に苦労することになった
    • 理解は進んだのでヨシ
    © 2017 Retrieva, Inc. 56

    View Slide

  57. 評価 evaluator.rs の実装解説
    • Rust の HashMap と所有権
    • map.insert(k, v) すると v の所有権は map に移るので、使えなくなる
    • map.get(k) で返るのは v の参照。所有権はまだ map にあるため
    • map.remove(k) すると所有権付きで帰ってくる
    © 2017 Retrieva, Inc. 57
    let mut map = HashMap::new();
    let value = "value".to_string();
    map.insert("key", value);
    value; // => Error: use of moved value: `value`
    map.get("key").unwrap(); // => &String
    map.remove("key").unwrap(); // => String

    View Slide

  58. 評価 evaluator.rs の実装解説
    • 環境の get/set するとき、所有権を渡さずに使いたい
    • 今考えると set の⽅はその後使わないはずなので直せそう
    • clone するしかない
    • と思う。もう少しうまくできるかもしれないが…
    • 識者に助⾔いただきたい…
    • clone する⽅針でやっていたら、再帰関数でこけることが判明
    • kei-s/waiir/commit/a1fdb15
    • Rc/RefCell を駆使して解決
    • 参照カウントと、(コンパイル時ではなく)実⾏時所有権規則検査の組み合わせ
    © 2017 Retrieva, Inc. 58

    View Slide

  59. まとめ
    © 2017 Retrieva, Inc. 59

    View Slide

  60. まとめ
    • 『Go⾔語で作るインタプリタ』を Rust で再現した
    • 付録のマクロシステムもボリュームあって⾯⽩い
    • インタプリタの勉強、Rust の勉強になった
    • Rust 難しいけど⾯⽩い
    • コンパイルエラーに導かれてコードを書くので、IDE 推奨(VSCode 使った)
    • 並⾏処理周りは触らなかったので気になる
    • 次のバージョンで async/await ⼊るらしい
    • 続編 “Write a compiler in Go”(未訳) というのもある
    • 構⽂解析までは本書のを使い、そこからコンパイラを作る
    • バイトコード、VM の実装をするらしい(先頭部分だけ読んだ)
    • やる…か…?
    © 2017 Retrieva, Inc. 60

    View Slide

  61. © 2017 Retrieva, Inc.

    View Slide