Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active September 8, 2025 11:36
Show Gist options
  • Select an option

  • Save mizchi/9e09758e0e8c934a885b3524a4646300 to your computer and use it in GitHub Desktop.

Select an option

Save mizchi/9e09758e0e8c934a885b3524a4646300 to your computer and use it in GitHub Desktop.

andromeda のコードを読んで、Claude Code にあってる?って確認させたもの

https://github.com/tryandromeda/andromeda

Andromeda とは

Parser に oxc, Runtime に Nova をベースにした、JS処理系。

位置づけとしては Node/Deno/Bun のようなもの。

https://wintertc.org/ つまりは Cloudflare Worker ぐらいの仕様を動くことを目指してる。

なぜ読んだか

成熟したランタイムはエラー処理が網羅的でコード量が多いので、学習に向かない。

こういう現状ではコアに近い処理しかないものの方が、同種のものを作る際の概要を掴むにはいい。


構成

  • Andromeda: TypeScript/JavaScript ランタイム全体のプロジェクト
  • Nova: ECMAScript エンジン (Rust製、V8やSpiderMonkeyに相当)
  • cli: コマンドラインインターフェース (cargo install --path cli でインストール)
  • core: ランタイムのコア実装(Node.js APIなどの提供)

イベントループ部分は tokio

First Step

$ cargo test --tests --workspace
$ cargo install --path cli
$ andromeda -h
$ echo "console.log(1)" > _scratch.ts
$ andromeda run _scratch.ts

Import

// console.log("scratch");

import { Queue } from "https://tryandromeda.dev/std/collections/mod.ts";
import { flatten } from "https://tryandromeda.dev/std/data/mod.ts";

const queue = new Queue();
queue.enqueue("first");
queue.enqueue("second");
console.log(queue.dequeue());
console.log(queue.size);

console.log(
  flatten(
    [
      [1, 2],
      [3, [4, 5]],
    ],
    1
  )
);
console.log(
  flatten(
    [
      [1, 2],
      [3, [4, 5]],
    ],
    2
  )
);

Code Reading

cli/src/main.rs

#[allow(clippy::result_large_err)]
fn run_main() -> Result<()> {
    /// tokio runtime と nova のスレッドを作成
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_time()
        .enable_io()
        .build()
        .map_err(|e| {
            error::AndromedaError::config_error(
                "Failed to initialize async runtime".to_string(),
                None,
                Some(Box::new(e)),
            )
        })?;

    // Run Nova in a secondary blocking thread so tokio tasks can still run
    let nova_thread = rt.spawn_blocking(move || -> Result<()> {
        match args.command {
            /// 実行部
            Command::Run {
                verbose,
                no_strict,
                paths,
            } => {
                let runtime_files: Vec<RuntimeFile> = paths
                    .into_iter()
                    .map(|path| RuntimeFile::Local { path })
                    .collect();
                run(verbose, no_strict, runtime_files)
            }
            // 静的解析
            Command::Check { paths } => {
                // Load configuration
                let config = ConfigManager::load_or_default(None);

                check_files_with_config(&paths, Some(config))
            }
            /...
        }
    });
    match rt.block_on(nova_thread) {
        Ok(result) => result,
        Err(e) => Err(error::AndromedaError::config_error(
            "Runtime execution failed".to_string(),
            None,
            Some(Box::new(e)),
        )),
    }
}
  • tokio runtime を作成(非同期I/O処理用)
  • nova thread を spawn_blocking で作成(JavaScriptエンジンはブロッキングスレッドで実行)
    • これにより tokio の非同期タスクと Nova の同期的な JavaScript 実行を両立

run()

cli/src/run.rs

#[allow(clippy::result_large_err)]
pub fn run(verbose: bool, no_strict: bool, files: Vec<RuntimeFile>) -> Result<()> {
    create_runtime_files(verbose, no_strict, files, None)
}
#[allow(clippy::result_large_err)]
pub fn create_runtime_files(
    verbose: bool,
    no_strict: bool,
    files: Vec<RuntimeFile>,
    config_override: Option<AndromedaConfig>,
) -> Result<()> {
    ///...
    let runtime = Runtime::new(
        RuntimeConfig {
            no_strict: effective_no_strict,
            files: filtered_files,
            verbose: effective_verbose,
            extensions: recommended_extensions(),
            builtins: recommended_builtins(),
            eventloop_handler: recommended_eventloop_handler,
            macro_task_rx,
            import_map,
        },
        host_data,
    );
    let mut runtime_output = runtime.run();
}
  • Runtime::new(...)
  • runtime.run()

core/src/runtime.rs

pub struct Runtime<UserMacroTask: 'static> {
    pub config: RuntimeConfig<UserMacroTask>,
    pub agent: GcAgent,
    pub realm_root: RealmRoot,
    pub host_hooks: &'static RuntimeHostHooks<UserMacroTask>,
}

正解! GcAgent を注入している。これは Nova のガベージコレクション管理の中心的な構造体。

impl<UserMacroTask> Runtime<UserMacroTask> {
    /// Create a new [Runtime] given a [RuntimeConfig]. Use [Runtime::run] to run it.
    pub fn new(
        mut config: RuntimeConfig<UserMacroTask>,
        host_data: HostData<UserMacroTask>,
    ) -> Self {
        /// import map の初期化
        let host_hooks = if let Some(import_map) = config.import_map.clone() {
            RuntimeHostHooks::with_import_map(host_data, base_path, import_map)
        } else {
            RuntimeHostHooks::with_base_path(host_data, base_path)
        };

        /// ... グローバル(globalThis)の初期化
        let create_global_object: Option<for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>> =
            None;
        let create_global_this_value: Option<
            for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>,
        > = None;
        let realm_root = agent.create_realm(
            create_global_object,
            create_global_this_value,
            Some(
                |agent: &mut Agent, global_object: Object<'_>, mut gc: GcScope<'_, '_>| {
                  ///...
                },
            ),
        );

        Self {
            config,
            agent,
            realm_root,
            host_hooks,
        }
    }
    ///...
}

一部正解、一部誤解:

  • ✅ Realm は正しく理解されています。ECMAScript仕様でグローバルオブジェクトとその環境を含む実行コンテキストの単位
  • ❌ globalThis は GC 管理外ではなく、GC 管理下にあります
    • create_global_objectcreate_global_this_value は GcScope を受け取るため、GC管理されるオブジェクトを生成
  • ✅ GC (GcAgent) は確かに Nova エンジンの中核的な部分で、メモリ管理を行う

runtime.run()

    pub fn run(mut self) -> RuntimeOutput {
        // Load the builtins js sources
        self.agent.run_in_realm(&self.realm_root, |agent, mut gc| {
            for builtin in &self.config.builtins {
                let realm = agent.current_realm(gc.nogc());
                let source_text = types::String::from_str(agent, builtin, gc.nogc());
                let script = match parse_script(
                    agent,
                    source_text,
                    realm,
                    !self.config.no_strict,
                    None,
                    gc.nogc(),
                ) {
                    Ok(script) => script,
                    Err(diagnostics) => exit_with_parse_errors(diagnostics, "<runtime>", builtin),
                };
                let eval_result = script_evaluation(agent, script.unbind(), gc.reborrow()).unbind();
                match eval_result {
                    Ok(_) => (),
                    Err(e) => {
                        let error_value = e.value();
                        let message = error_value
                            .string_repr(agent, gc.reborrow())
                            .as_str(agent)
                            .unwrap_or("<non-string error>")
                            .to_string();
                        println!("Error in runtime: {message}");
                    }
                }
            }
        });
  • realm.run_in_realm(...)
  • parse_script(...)
  • script_evaluation(...)

正解! Agent が全体のマネージャで、script を生成して評価。

  • parse_script: ソースコードをパースして Script 構造体を生成
  • script_evaluation: パースされた Script を実際に評価して実行
  • eval_result: 実行結果 (Ok なら正常終了、Err ならエラー値)

Agent

正解! ECMAScript 仕様 (tc39.es/ecma262) の Agent 概念の実装

/// ### [9.7 Agents](https://tc39.es/ecma262/#sec-agents)
pub struct Agent {
    pub(crate) heap: Heap,
    pub(crate) options: Options,
    pub(crate) symbol_id: usize,
    pub(crate) global_symbol_registry: AHashMap<&'static str, Symbol<'static>>,
    pub(crate) host_hooks: &'static dyn HostHooks,
    execution_context_stack: Vec<ExecutionContext>,
    /// Temporary storage for on-stack heap roots.
    pub fn run_in_realm<F, R>(&mut self, realm: &RealmRoot, func: F) -> R
    where
        F: for<'agent, 'gc, 'scope> FnOnce(&'agent mut Agent, GcScope<'gc, 'scope>) -> R,
    {
        let realm = self.get_realm_by_root(realm);
        assert!(self.agent.execution_context_stack.is_empty());
        let result = self.agent.run_in_realm(realm, func);
        clear_kept_objects(&mut self.agent);
        assert!(self.agent.execution_context_stack.is_empty());
        assert!(self.agent.vm_stack.is_empty());
        self.agent.stack_refs.borrow_mut().clear();
        result
    }

正解!

  • agent.run_in_realm() でレルム内でコードを実行
  • assert! で実行コンテキストスタックが空になっていることを確認(実行前後でスタックが正しくクリーンアップされていることを保証)

agent.run_in_realm

    pub fn run_in_realm<F, R>(&mut self, realm: Realm, func: F) -> R
    where
        F: for<'agent, 'gc, 'scope> FnOnce(&'agent mut Agent, GcScope<'gc, 'scope>) -> R,
    {
        let execution_stack_depth_before_call = self.execution_context_stack.len();
        self.push_execution_context(ExecutionContext {
            ecmascript_code: None,
            function: None,
            realm: realm.unbind(),
            script_or_module: None,
        });
        let (mut gc, mut scope) = unsafe { GcScope::create_root() };
        let gc = GcScope::new(&mut gc, &mut scope);

        let result = func(self, gc);
        assert_eq!(
            self.execution_context_stack.len(),
            execution_stack_depth_before_call + 1
        );
        self.pop_execution_context();
        result
    }

ほぼ正解!

  • ✅ 実行コンテキストをスタックにプッシュ(キューではなくスタック)
  • ✅ 新しい GC スコープを生成(GcScope::create_root で GC のルートスコープを作成)
  • func(self, gc) でクロージャを実行(このクロージャ内で実際の JavaScript コードが実行される)

この func は for<'agent, 'gc, 'scope> FnOnce(&'agent mut Agent, GcScope<'gc, 'scope>) -> R で高階関数になってる。

これをどう作ってたか遡って確認すると

core/src/runtime.rs

        self.agent.run_in_realm(&self.realm_root, |agent, mut gc| {
            for builtin in &self.config.builtins {
                let realm = agent.current_realm(gc.nogc());
                let source_text = types::String::from_str(agent, builtin, gc.nogc());

正解! ビルトインは JavaScript の文字列コードとして渡されている(おそらく Polyfill や標準ライブラリの実装)。

Nova Engine: parse_script

これは nova 側の実装になる

正解! Script がヒープ上の SourceCode への参照を保持するため unsafe が必要。 Rust の借用チェッカーでは表現できない「ヒープ上のデータの生存期間保証」を手動で管理している。

pub fn parse_script<'a>(
    agent: &mut Agent,
    source_text: String,
    realm: Realm,
    strict_mode: bool,
    host_defined: Option<HostDefined>,
    gc: NoGcScope<'a, '_>,
) -> ScriptOrErrors<'a> {
    ///...
    // SAFETY: Script keeps the SourceCode reference alive in the Heap, thus
    // making the Program's references point to a live Allocator.
    let parse_result = unsafe {
        SourceCode::parse_source(
            agent,
            source_text,
            source_type,
            #[cfg(feature = "typescript")]
            true,
            gc,
        )
    };

    let ParseResult {
        source_code,
        body,
        directives: _,
        is_strict,
    } = match parse_result {
        // 2. If script is a List of errors, return script.
        Ok(result) => result,
        Err(errors) => {
            return Err(errors);
        }
    };

    // 3. Return Script Record {
    let script_record = ScriptRecord {
        // [[Realm]]: realm,
        realm: realm.unbind(),
        // [[ECMAScriptCode]]: script,
        // SAFETY: We are moving the statements slice onto the heap together
        // with the SourceCode reference: the latter will keep alive the
        // allocation that Statements point to. Hence, we can unbind the
        // Statements from the garbage collector lifetime here.
        ecmascript_code: NonNull::from(body),
        is_strict,
        // [[LoadedModules]]: « »,
        loaded_modules: Default::default(),
        // [[HostDefined]]: hostDefined,
        host_defined,
        source_code: source_code.unbind(),
    };
    // }
    let script = agent.heap.add_script(script_record, gc);
    Ok(script)
}
  • SourceCode::parse_source() -> ParseSource
  • ScriptRecord

SourceCode の定義

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct SourceCode<'a>(BaseIndex<'a, SourceCodeHeapData<'static>>);

正解! #[repr(transparent)] は内部の BaseIndex と同じメモリレイアウトを保証。 これにより、SourceCode は BaseIndex へのゼロコスト抽象化となり、ヒープデータへの型安全なポインタとして機能。

Nova: script_evaluation

/// ### [16.1.6 ScriptEvaluation ( scriptRecord )](https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation)
///
/// The abstract operation ScriptEvaluation takes argument scriptRecord (a
/// Script Record) and returns either a normal completion containing an
/// ECMAScript language value or an abrupt completion.
pub fn script_evaluation<'a>(
    agent: &mut Agent,
    script: Script,
    mut gc: GcScope<'a, '_>,
) -> JsResult<'a, Value<'a>> {
    ///...
}

正解! Agent(実行環境)、Script(パース済みコード)、GC(メモリ管理)の3つがあれば JavaScript を実行可能。

実行コンテキストの作成

let script_context = ExecutionContext {...}

ここで実行結果がとれている

    let result = unsafe {
        global_declaration_instantiation(agent, script.unbind(), global_env.unbind(), gc.reborrow())
            .unbind()
            .bind(gc.nogc())
    };

良い観点! global_declaration_instantiation は:

  • グローバルスコープの変数宣言を処理
  • varfunction 宣言をグローバルオブジェクトにバインド
  • REPL モードでは各入力で新たな宣言を追加可能にする必要がある

Runtime.run に戻る

正解! ここまではビルトイン(組み込み JavaScript ライブラリ)の初期化のみ。

        // Load the builtins js sources
        self.agent.run_in_realm(&self.realm_root, |agent, mut gc| {

ここでユーザーコードが実行される

        // Fetch the runtime mod.ts file using a macro and add it to the paths
        for file in &self.config.files {
            let file_content = match file.read() {
                Ok(content) => content,
                Err(error) => {
                    eprintln!("🚨 Failed to read file {}: {}", file.get_path(), error);
                    std::process::exit(1);
                }
            };

            ///...
            result = self.agent.run_in_realm(&self.realm_root, |agent, mut gc| {
                let source_text = types::String::from_string(agent, file_content, gc.nogc());
                let realm = agent.current_realm(gc.nogc());

正解!

  • ビルトイン: parse_script でスクリプトモード(グローバルスコープに変数を定義)
  • ユーザーコード: parse_module でモジュールモード(ESモジュール、strict mode、import/export 対応)
                let module = match parse_module(
                    agent,
                    source_text,
                    realm,
                    Some(std::rc::Rc::new(file.get_path().to_string()) as HostDefined),
                    gc.nogc(),
                ) {
                    Ok(module) => module,
                    Err(errors) => exit_with_parse_errors(
                        errors,
                        file.get_path(),
                        source_text
                            .as_str(agent)
                            .expect("String is not valid UTF-8"),
                    ),
                };

この agent.run_parsed_module() が実行部分

                agent
                    .run_parsed_module(module.unbind(), None, gc.reborrow())
                    .unbind()
                    .map(|_| Value::Null)

Nova run_parsed_module

    /// Run a parsed SourceTextModule in the current Realm.
    ///
    /// This runs the LoadRequestedModules (passing in the host_defined
    /// parameter), Link, and finally Evaluate operations on the module.
    /// This should not be called multiple times on the same module.
    pub fn run_parsed_module<'gc>(
        &mut self,
        module: SourceTextModule,
        host_defined: Option<HostDefined>,
        mut gc: GcScope<'gc, '_>,
    ) -> JsResult<'gc, Value<'gc>> {
        let module = module.bind(gc.nogc());
        let Some(result) = module
            .load_requested_modules(self, host_defined, gc.nogc())
            .try_get_result(self, gc.nogc())
        else {
            return Err(self.throw_exception_with_static_message(
                ExceptionType::Error,
                "module was not sync",
                gc.into_nogc(),
            ));
        };

GC reborrow の理由: Rust の借用規則を満たしながら GC スコープを複数の関数に渡すため。

  • gc.reborrow() で新しい借用を作成(元の gc は一時的に使用不可)
  • 各関数が独自の GC スコープ借用を持てるようになる
  • これにより、ネストした関数呼び出しでも安全に GC を使用可能

runtime.run ~ loop

正解! イベントループの実装:Promise のマイクロタスクとマクロタスクのキューを処理

        loop {
            while let Some(job) = self.host_hooks.pop_promise_job() {
                result = self.agent.run_in_realm(&self.realm_root, |agent, mut gc| {
                    job.run(agent, gc.reborrow()).unbind().map(|_| Value::Null)
                });
            }

            // If both the microtasks and macrotasks queues are empty we can end the event loop
            if !self.host_hooks.any_pending_macro_tasks() {
                break;
            }

            self.handle_macro_task();
        }

pop_promise_job の返す Job の定義を確認

pub struct Job {
    pub(crate) realm: Option<Realm<'static>>,
    pub(crate) inner: InnerJob,
}

pub(crate) enum InnerJob {
    PromiseResolveThenable(PromiseResolveThenableJob),
    PromiseReaction(PromiseReactionJob),
}

#[derive(Debug)]
pub(crate) struct PromiseResolveThenableJob {
    promise_to_resolve: Global<Promise<'static>>,
    thenable: Global<Object<'static>>,
    then: Global<Function<'static>>,
}

/// then: Global<Function<'static>> の関数定義実体をみてみる
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Function<'a> {
    BoundFunction(BoundFunction<'a>) = BOUND_FUNCTION_DISCRIMINANT,
    BuiltinFunction(BuiltinFunction<'a>) = BUILTIN_FUNCTION_DISCRIMINANT,
    ECMAScriptFunction(ECMAScriptFunction<'a>) = ECMASCRIPT_FUNCTION_DISCRIMINANT,
    BuiltinConstructorFunction(BuiltinConstructorFunction<'a>) =
        BUILTIN_CONSTRUCTOR_FUNCTION_DISCRIMINANT,
    BuiltinPromiseResolvingFunction(BuiltinPromiseResolvingFunction<'a>) =
        BUILTIN_PROMISE_RESOLVING_FUNCTION_DISCRIMINANT,
    BuiltinPromiseFinallyFunction(BuiltinPromiseFinallyFunction<'a>) =
        BUILTIN_PROMISE_FINALLY_FUNCTION_DISCRIMINANT,
    BuiltinPromiseCollectorFunction = BUILTIN_PROMISE_COLLECTOR_FUNCTION_DISCRIMINANT,
    BuiltinProxyRevokerFunction = BUILTIN_PROXY_REVOKER_FUNCTION,
}

正解! ディスクリミナント(判別値)を使った効率的な型判定の実装。 各関数型に固有の定数値を割り当てて、高速な型チェックを実現。

pub(crate) const BUILTIN_PROXY_REVOKER_FUNCTION: u8 =
    value_discriminant(Value::BuiltinProxyRevokerFunction);
nova_vm::ecmascript::types::language::value
const fn value_discriminant(value: Value) -> u8

正解! Promise の then メソッドが Promise ジョブキューに登録される仕組み。 これが JavaScript の Promise チェーンとマイクロタスクキューの実装の核心部分。

core/src/runtime.rs

    // Listen for pending macro tasks and resolve one by one
    pub fn handle_macro_task(&mut self) {
        match self.config.macro_task_rx.recv() {
            Ok(MacroTask::ResolvePromise(root_value)) => {
                self.agent.run_in_realm(&self.realm_root, |agent, gc| {
                    let value = root_value.take(agent);
                    if let Value::Promise(promise) = value {
                        let promise_capability = PromiseCapability::from_promise(promise, false);
                        promise_capability.resolve(agent, Value::Undefined, gc);
                    } else {
                        panic!("Attempted to resolve a non-promise value");
                    }
                });
            }
            // Let the user runtime handle its macro tasks
            Ok(MacroTask::User(e)) => {
                (self.config.eventloop_handler)(
                    e,
                    &mut self.agent,
                    &self.realm_root,
                    &self.host_hooks.host_data,
                );
            }
            _ => {}
        }
    }

cli/src/run.rs runtime_output を処理する部分

    ///...
    let mut runtime_output = runtime.run();

    match runtime_output.result {
        Ok(result) => {
            if effective_verbose {
                println!("✅ Execution completed successfully: {result:?}");
            }
            Ok(())
        }
        Err(error) => {
            // Extract detailed error information from Nova
            let error_message =
                runtime_output
                    .agent
                    .run_in_realm(&runtime_output.realm_root, |agent, gc| {
                        error
                            .value()
                            .string_repr(agent, gc)
                            .as_str(agent)
                            .expect("String is not valid UTF-8")
                            .to_string()
                    });

            // Try to get the first file from our runtime files to show source context
            let (file_path, source_content) = if let Some(path) = first_file_info {
                match read_file_with_context(std::path::Path::new(&path)) {
                    Ok(content) => (Some(path), Some(content)),
                    Err(_) => (Some(path), None),
                }
            } else {
                (None, None)
            };

正解! エラー時の処理:

  • Nova エンジンからの生のエラー値を取得
  • string_repr でエラーを文字列表現に変換
  • ソースコードコンテキストを読み込んで、エラー位置を表示

全体アーキテクチャのまとめ

レイヤー構造

  1. CLI層 (cli/): コマンドライン処理、ファイル読み込み
  2. Core層 (core/): ランタイム管理、イベントループ、ホスト環境統合
  3. Nova層 (nova_vm/): ECMAScript エンジン実装

実行フロー

  1. 初期化フェーズ

    • Tokio ランタイム起動(非同期I/O用)
    • Nova スレッド作成(JavaScriptエンジン用)
    • GcAgent 初期化(メモリ管理)
    • Realm 作成(グローバル環境)
    • ビルトイン読み込み(標準ライブラリ)
  2. 実行フェーズ

    • ソースコードパース(parse_module/parse_script)
    • モジュール評価(module evaluation)
    • イベントループ開始
  3. イベントループ

    • マイクロタスク処理(Promise解決)
    • マクロタスク処理(タイマー、I/O等)
    • 全タスク完了まで継続

GC とメモリ管理

  • GcScope: ガベージコレクションのスコープ管理
    • gc.nogc(): GC を一時的に無効化(パフォーマンス重要な箇所)
    • gc.reborrow(): 新しい借用を作成(ネストした関数呼び出し用)
  • unbind/bind: ライフタイム管理
    • unbind(): ライフタイムを static に変換(ヒープ保存用)
    • bind(): static から具体的なライフタイムに復元
  • Global: GC 管理下のグローバル参照(Promise等の長寿命オブジェクト)

型システムの工夫

  • ディスクリミナント: 高速な型判定のための定数値
  • repr(transparent): ゼロコスト抽象化
  • unsafe: ヒープ参照の手動管理(Rust の借用チェッカーの限界を超える場合)

モジュールシステム

  • スクリプトモード: ビルトイン用、グローバルスコープ
  • モジュールモード: ユーザーコード用、ESモジュール対応、strict mode

これで Andromeda/Nova の基本的なアーキテクチャは理解できています!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment