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

Bf7ce9b68bbb0058721906b4db15b9b5?s=128

Kei Shiratsuchi

August 21, 2019
Tweet

Transcript

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

  2. ⾃⼰紹介 • ⽩⼟ 慧(シラツチ ケイ) • 略歴 • 2008-2011 サイジニア株式会社

    • 2012-2016 株式会社オーマイグラス • 2016/4- PFI → 株式会社レトリバ • Ruby on Rails / JavaScript • 趣味:通勤中の読書・映画 • 「マイホームヒーロー 」⼭川直輝原作、朝基まさし作画 • 「⼤統領失踪」ビル・クリントン、ジェイムズ・パタースン • 「三体」劉慈欣 • 「Fake」森達也「淋しいのはアンタだけじゃない」吉本浩⼆ © 2017 Retrieva, Inc. 2
  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
  4. アジェンダ © 2017 Retrieva, Inc. 4

  5. アジェンダ • 『Go⾔語で作るインタプリタ』 • Rust • 今回作る⾔語 “Monkey” の紹介 •

    インタプリタの流れ • 字句解析 • 構⽂解析 • 評価 • まとめ © 2017 Retrieva, Inc. 5
  6. Go⾔語で作るインタプリタ © 2017 Retrieva, Inc. 6

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

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

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

    Retrieva, Inc. 9
  10. Rust • Mozilla が開発を主導 • ベターCを⽬指すっぽい(分野としてC++と競合しそう) • 安全性、速度、並⾏性の3つのゴールにフォーカス • ガーベジコレクタなしにこれらのゴールを実現

    • 他の⾔語への埋め込み、要求された空間や時間内での動作、 デバイス ドライバやオペレーティングシステムのような低レベルなコードを得 意とする • 全てのデータ競合を排除しつつも実⾏時オーバーヘッドのないコンパ イル時の安全性検査を多数持つ © 2017 Retrieva, Inc. 10
  11. Rust の特徴:所有権と借⽤ • 変数の「所有権」をやり取りすることで、メモリ安全性を確保する • 違反する場合、コンパイル時にエラーにする • これにより、マルチスレッドでのデータ競合などを未然に防ぐ • 「しかし、このシステムはあるコストを持ちます。それは学習曲線です。

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

    多くの新しいRustのユーザは「借⽤チェッカとの戦い」と好んで呼ばれる ものを経験します。 … … … しかし、よいニュースがあります。… … …⼀度彼らが所有権システムの ルールとともにしばらく仕事をすれば、彼らが借⽤チェッカと戦うことは 少なくなっていくということです。」 © 2017 Retrieva, Inc. 12
  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<i32>) { println!(”v{}", v[0]); } fn main() { let v = vec![1, 2, 3]; take(v); println!("v[0] is: {}", v[0]); }
  14. Rust:参照と借⽤ • v を渡す代わりに、&v を渡す。引数にも &Vec<i32> を使う。 • &T, &mut

    T は参照。所有権を「借⽤」する。 © 2017 Retrieva, Inc. 14 fn take(v: &Vec<i32>) { 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); }
  15. P.11「はじめに」 © 2017 Retrieva, Inc. 15

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

  17. 謝辞 • Rust で実装した偉⼤なる先⼈ • kogai/monkey • tsuyoshiwada/rs-monkey-lang • avocado-shrimp/Monkey

    • めちゃめちゃ参考にさせていただきました © 2017 Retrieva, Inc. 17
  18. “Monkey” © 2017 Retrieva, Inc. 18

  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⾔語⾵の構⽂ • 変数束縛 • 整数と真偽値 • 算術式 • ⽂字列データ型 • 配列データ型 • ハッシュデータ型 • 関数
  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); } } }; • クロージャ • 関数の再帰呼び出し • 第⼀級の⾼階関数
  21. デモ • cargo build --release して target/release/waiir で REPL 起動

    • もしくは cargo run • 複数⾏を受け付けられないので注意 © 2017 Retrieva, Inc. 21
  22. インタプリタの流れ © 2017 Retrieva, Inc. 22

  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
  24. 字句解析 © 2017 Retrieva, Inc. 24

  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
  26. 字句解析 • ソースコードの⽂字列を1⽂字ずつ⼿前から順に読んでいき、 トークン列を作成する • 記号 => 対応する記号 (+, -,

    =, ==, !=, …) のトークン • “ => “ で終わるまでの⽂字列の String トークン • ⽂字列 => 予約語 (let, return, fn, …) ならそのトークン そうでなければ Ident トークン • 数字 => 数値トークン • Monkey では整数のみ扱い、⼩数、8進数、16進数などは使えないとする • 空⽩、タブ⽂字、改⾏ => 読み⾶ばす © 2017 Retrieva, Inc. 26
  27. 字句解析 lexer.rs の実装解説 • ⽂字を取り出す際、Go では⽂字列 input のインデックスアク セスで良いが、 •

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

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

  30. 構⽂解析 • yacc, bison などのパーサジェネレータを使わず、⼀から作る • BNF 書かない • 再帰下降構⽂解析という⼿法の、Pratt構⽂解析を実装

    • 昔から存在していたが、近年 JSLint の実装に使われたり再発⾒された © 2017 Retrieva, Inc. 30
  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
  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
  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
  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
  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;
  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 }
  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
  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 } 対応する関数を実⾏する 対応する関数を実⾏する + - ( トークンごとに 前置演算⼦、中置演算⼦に来た時 パースする関数を定義する
  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 をアサインする
  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 } 中置演算⼦の処理にて 今パースしている式の優先順位が 次のトークンの優先順位より⾼くなるま で、 この関数が返す式を更新する
  41. 構⽂解析 parser.rs の実装解説 • 本では途中、未実装のトークンが来た場合に nil を返す • Rust には

    NULL がないため無視するとコンパイルできない。 その時点でもエラー処理を考えないといけない • エラー処理は Result 型を使う • prefixParseFuncs を作らず、都度 match して分岐させる • HashMap に渡すものの型もしっかり定義しないといけない • (作り終わった今ならできそうな気もするが…) © 2017 Retrieva, Inc. 41
  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 から実体を取れない
  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) => ..., } }
  44. 評価 © 2017 Retrieva, Inc. 44

  45. 評価 • 構築した抽象構⽂⽊を評価する • インタプリタとコンパイラの境界は実は曖昧 • インタプリタでも、 AST 書き換えたり、中間表現(IR)へ変換したり、 JITコンパイラで⼀部をコンパイルしたり

    • Monkey は、tree-walking インタプリタ • Ruby 1.8 まで採⽤されていた⽅式 © 2017 Retrieva, Inc. 45
  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 } } } }
  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 } }
  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) … }
  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 …
  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 } } }
  51. 関数呼び出し • の前に、期待する動作 • 何も考えず引数を今の環境に上書きすると、同じ i なので関数を抜けたあとも 10 のままになる •

    関数の引数は、元の環境を保持したまま新しい環境を作成する必要がある • 関数内での識別⼦の呼び出しは、新しい環境から探し、⾒つからない場合は元の環境に フォールバックして探す • => クロージャ!!! © 2017 Retrieva, Inc. 51 let i = 5; let printNum = fn(i) { puts(i); }; printNum(10); //=> 10 puts(i); //=> 5
  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 }
  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 } 外側の環境を保持するフィールドを追加 外側の環境を保持した環境を返す 現在の環境で値が⾒つからず 外側の環境があるとき 外側の環境から探してくる
  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 を渡す 作成した環境で関数の中⾝を評価
  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
  56. 評価 evaluator.rs の実装解説 • Go では eval(ast.Node) のように、全ての AST ノードが

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

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

  60. まとめ • 『Go⾔語で作るインタプリタ』を Rust で再現した • 付録のマクロシステムもボリュームあって⾯⽩い • インタプリタの勉強、Rust の勉強になった

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