andromeda のコードを読んで、Claude Code にあってる?って確認させたもの
https://github.com/tryandromeda/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
$ cargo test --tests --workspace
$ cargo install --path cli
$ andromeda -h
$ echo "console.log(1)" > _scratch.ts
$ andromeda run _scratch.ts// 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
)
);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 実行を両立
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_objectとcreate_global_this_valueは GcScope を受け取るため、GC管理されるオブジェクトを生成
- ✅ GC (GcAgent) は確かに Nova エンジンの中核的な部分で、メモリ管理を行う
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 ならエラー値)
正解! 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! で実行コンテキストスタックが空になっていることを確認(実行前後でスタックが正しくクリーンアップされていることを保証)
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 側の実装になる
正解! 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 へのゼロコスト抽象化となり、ヒープデータへの型安全なポインタとして機能。
/// ### [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 は:
- グローバルスコープの変数宣言を処理
var、function宣言をグローバルオブジェクトにバインド- REPL モードでは各入力で新たな宣言を追加可能にする必要がある
正解! ここまではビルトイン(組み込み 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) /// 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 を使用可能
正解! イベントループの実装: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,
);
}
_ => {}
}
} ///...
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でエラーを文字列表現に変換- ソースコードコンテキストを読み込んで、エラー位置を表示
- CLI層 (cli/): コマンドライン処理、ファイル読み込み
- Core層 (core/): ランタイム管理、イベントループ、ホスト環境統合
- Nova層 (nova_vm/): ECMAScript エンジン実装
-
初期化フェーズ
- Tokio ランタイム起動(非同期I/O用)
- Nova スレッド作成(JavaScriptエンジン用)
- GcAgent 初期化(メモリ管理)
- Realm 作成(グローバル環境)
- ビルトイン読み込み(標準ライブラリ)
-
実行フェーズ
- ソースコードパース(parse_module/parse_script)
- モジュール評価(module evaluation)
- イベントループ開始
-
イベントループ
- マイクロタスク処理(Promise解決)
- マクロタスク処理(タイマー、I/O等)
- 全タスク完了まで継続
- GcScope: ガベージコレクションのスコープ管理
gc.nogc(): GC を一時的に無効化(パフォーマンス重要な箇所)gc.reborrow(): 新しい借用を作成(ネストした関数呼び出し用)
- unbind/bind: ライフタイム管理
unbind(): ライフタイムを static に変換(ヒープ保存用)bind(): static から具体的なライフタイムに復元
- Global: GC 管理下のグローバル参照(Promise等の長寿命オブジェクト)
- ディスクリミナント: 高速な型判定のための定数値
- repr(transparent): ゼロコスト抽象化
- unsafe: ヒープ参照の手動管理(Rust の借用チェッカーの限界を超える場合)
- スクリプトモード: ビルトイン用、グローバルスコープ
- モジュールモード: ユーザーコード用、ESモジュール対応、strict mode
これで Andromeda/Nova の基本的なアーキテクチャは理解できています!