Dev Study
← Rust で小さなブラウザを作る コース

26. 評価と環境 — 変数の表を外側へ辿り、関数呼び出しごとに新しい表を作る

()は を根から辿って値にします。数値リテラルは Number、文字列リテラルは Str、はから値を引く、BinaryExpression は左右を評価して + か - を適用する、という素直な再帰です。値は RuntimeValue(Undefined / Null / Number / Str / Function / Builtin / Object)で表します。

変数の置き場所が環境(Environment)です。名前から値への と、外側の環境への参照(outer)を持ちます。get は自分の表に無ければ外側へ辿る()、define は自分の表に作る(var / function / 仮引数)、assign は見つかった環境の値を書き換える。 §9 の Environment Record の、必要な部分だけです。グローバルの環境には最初から console(と、 があれば )が入れてあります。

関数呼び出し(call)は、Function が持つ (定義されたときの環境)を外側にして新しい環境を作り、仮引数に実引数を define し、本体を実行します。本体の中の var は新しい環境に入るので外に漏れず、外側の変数は outer を辿って読めます。return は Completion::Return として呼び出し元まで持ち上げ、無ければ Undefined です。関数はの先頭で先に define するので、宣言より前の行から呼べます()。

+ は ECMAScript の規則の縮小版で、どちらかが文字列なら連結、そうでなければ数の足し算です('a' + 1 + 2 は "a12")。宣言していない名前を読むと ReferenceError、関数でないものを呼ぶと TypeError を返します。前のステップ(step13)を実行すると、11 のプログラムの結果と console.log の出力、2 つのエラーが出ます。

本物のブラウザでは: はまず (インタプリタ)でを実行し、実行中に集めた型や形の情報()をもとに、よく呼ばれる関数を → → と段階的に機械語へコンパイルします(v8.dev/blog/maglev)。 は Interpreter → (インラインキャッシュ付き)→ WarpMonkey の順です。この回の Environment の連鎖は V8 では Context と ScopeInfo(src/objects/contexts.h、scope-info.h)、SpiderMonkey では EnvironmentObject と Scope にあたります。メモリは V8 の が(nursery を Scavenger で、全体を Mark-Compact で)に回収し、並列・増分・並行で停止時間を削っています(v8.dev/blog/trash-talk)。 で済ませたこの実装には GC がありません。

開発のどこに活かすか: 「スコープチェーン」「クロージャは定義時の環境を持つ」「関数宣言の巻き上げ」は JavaScript の面接の定番ですが、環境を自分で HashMap と outer で作ると、暗記ではなく仕組みとして残ります。テンプレートエンジンの変数展開、設定ファイルの変数参照、ワークフローの式評価のような「つきの評価器」は、この Environment と eval の形をそのまま流用できます。

点線の付いた語はタップすると説明が出ます。用語集を開く →

① var base = 10; function f(a) { return base + a; } f(5) の環境

f を呼んだ瞬間の環境のつながり
グローバル環境console, document, base = 10, f = Function{closure: グローバル}
f(5) で新しい環境を作る(outer = f の closure = グローバル)
f の呼び出しの環境a = 5
base + a の評価
a は自分の表にある → 5
base は無い → outer を辿る → 10
Completion::Return(15)
呼び出し元に 15
  • ・f の中で var inner = 1 としても、入るのは呼び出しの環境。外から inner を読むと ReferenceError
  • ・呼び出しが終わると環境は捨てられる(Rc の参照が 0 になる)

「名前をどこで探すか」が outer の鎖で決まるので、同じ名前でも中と外で別の変数になります。

サンプルコード(フレームワーク環境が必要なため表示のみ)

// src/js/runtime.rs の環境・ブロックの実行・関数呼び出し
impl Environment {
    pub fn new(outer: Option<EnvRef>) -> EnvRef {
        Rc::new(RefCell::new(Environment {
            values: HashMap::new(),
            outer,
        }))
    }

    /// 名前を探す。無ければ外側へ(スコープチェーン)
    pub fn get(&self, name: &str) -> Option<RuntimeValue> {
        match self.values.get(name) {
            Some(v) => Some(v.clone()),
            None => self.outer.as_ref().and_then(|o| o.borrow().get(name)),
        }
    }

    /// この環境に新しく定義する(var / function / 仮引数)
    pub fn define(&mut self, name: &str, value: RuntimeValue) {
        self.values.insert(name.to_string(), value);
    }

    /// 既存の変数に代入する。見つかった環境で書き換える。どこにも無ければ false
    pub fn assign(&mut self, name: &str, value: RuntimeValue) -> bool {
        if self.values.contains_key(name) {
            self.values.insert(name.to_string(), value);
            return true;
        }
        match &self.outer {
            Some(o) => o.borrow_mut().assign(name, value),
            None => false,
        }
    }
}

fn exec_block(&mut self, statements: &[Node], env: &EnvRef) -> Result<Completion, JsError> {
    // 関数宣言は巻き上げ(hoisting)される: ブロックの先頭で先に定義する
    for s in statements {
        if let Node::FunctionDeclaration { name, params, body } = s {
            env.borrow_mut().define(
                name,
                RuntimeValue::Function {
                    params: params.clone(),
                    body: body.clone(),
                    closure: env.clone(),
                },
            );
        }
    }
    let mut last = RuntimeValue::Undefined;
    for s in statements {
        match self.exec_statement(s, env)? {
            // 関数宣言と変数宣言は「値を持たない文」。最後の値を上書きしない(ECMAScript の empty completion)
            Completion::Normal(_) if matches!(s, Node::FunctionDeclaration { .. } | Node::VariableDeclaration { .. }) => {}
            Completion::Normal(v) => last = v,
            ret @ Completion::Return(_) => return Ok(ret),
        }
    }
    Ok(Completion::Normal(last))
}

fn call(&mut self, f: RuntimeValue, args: Vec<RuntimeValue>) -> Result<RuntimeValue, JsError> {
    match f {
        RuntimeValue::Function { params, body, closure } => {
            // 呼び出しごとに新しい環境。外側は「定義されたときの環境」(字句スコープ)
            let local = Environment::new(Some(closure));
            for (i, p) in params.iter().enumerate() {
                let v = args.get(i).cloned().unwrap_or(RuntimeValue::Undefined);
                local.borrow_mut().define(p, v);
            }
            match self.exec_block(&body, &local)? {
                Completion::Return(v) => Ok(v),
                Completion::Normal(_) => Ok(RuntimeValue::Undefined), // return が無ければ undefined
            }
        }
        RuntimeValue::Builtin(Builtin::ConsoleLog) => {
            let line = args.iter().map(|a| a.to_string()).collect::<Vec<_>>().join(" ");
            self.console_output.push(line);
            Ok(RuntimeValue::Undefined)
        }
        RuntimeValue::Builtin(Builtin::GetElementById) => {
            let id = args.first().map(|a| a.to_string()).unwrap_or_default();
            let doc = self
                .document
                .clone()
                .ok_or_else(|| JsError::ReferenceError("document is not defined".into()))?;
            Ok(match DomNode::get_element_by_id(&doc, &id) {
                Some(n) => RuntimeValue::Object(JsObject::Element(n)),
                None => RuntimeValue::Null, // 見つからなければ null(仕様どおり)
            })
        }
        other => Err(JsError::TypeError(format!("{other} is not a function"))),
    }
}

手元で動かす(配布物)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗