diff --git a/src/box_factory/builtin.rs b/src/box_factory/builtin.rs index 99698660..cdad31ff 100644 --- a/src/box_factory/builtin.rs +++ b/src/box_factory/builtin.rs @@ -581,6 +581,8 @@ impl BoxFactory for BuiltinBoxFactory { fn box_types(&self) -> Vec<&str> { self.creators.keys().map(|s| s.as_str()).collect() } + + fn is_builtin_factory(&self) -> bool { true } } /// Declarative macro for registering multiple Box types at once diff --git a/src/box_factory/mod.rs b/src/box_factory/mod.rs index 5a7a1975..9e2a9b22 100644 --- a/src/box_factory/mod.rs +++ b/src/box_factory/mod.rs @@ -36,6 +36,11 @@ pub trait BoxFactory: Send + Sync { fn supports_birth(&self) -> bool { true } + + /// Identify builtin factory to enforce reserved-name protections + fn is_builtin_factory(&self) -> bool { + false + } } /// Registry that manages all BoxFactory implementations @@ -64,10 +69,41 @@ impl UnifiedBoxRegistry { // Update cache let mut cache = self.type_cache.write().unwrap(); + // Reserved core types that must remain builtin-owned + fn is_reserved_type(name: &str) -> bool { + matches!( + name, + // Core value types + "StringBox" | "IntegerBox" | "BoolBox" | "FloatBox" | "NullBox" + // Core containers and result + | "ArrayBox" | "MapBox" | "ResultBox" + // Core method indirection + | "MethodBox" + ) + } for type_name in types { + // Enforce reserved names: only builtin factory may claim them + if is_reserved_type(type_name) && !factory.is_builtin_factory() { + eprintln!( + "[UnifiedBoxRegistry] ❌ Rejecting registration of reserved type '{}' by non-builtin factory #{}", + type_name, factory_index + ); + continue; + } + // First registered factory wins (priority order) - cache.entry(type_name.to_string()) - .or_insert(factory_index); + let entry = cache.entry(type_name.to_string()); + use std::collections::hash_map::Entry; + match entry { + Entry::Occupied(existing) => { + // Collision: type already claimed by earlier factory + eprintln!("[UnifiedBoxRegistry] ⚠️ Duplicate registration for '{}': keeping factory #{}, ignoring later factory #{}", + existing.key(), existing.get(), factory_index); + } + Entry::Vacant(v) => { + v.insert(factory_index); + } + } } self.factories.push(factory); @@ -113,6 +149,26 @@ impl UnifiedBoxRegistry { message: format!("Unknown Box type: {}", name), }) } + + /// Check whether a type name is known to the registry + pub fn has_type(&self, name: &str) -> bool { + // Check cache first + { + let cache = self.type_cache.read().unwrap(); + if let Some(&idx) = cache.get(name) { + if let Some(factory) = self.factories.get(idx) { + if factory.is_available() { return true; } + } + } + } + // Fallback: scan factories that can enumerate types + for factory in &self.factories { + if !factory.is_available() { continue; } + let types = factory.box_types(); + if !types.is_empty() && types.contains(&name) { return true; } + } + false + } /// Get all available Box types pub fn available_types(&self) -> Vec { @@ -144,4 +200,4 @@ mod tests { let registry = UnifiedBoxRegistry::new(); assert_eq!(registry.available_types().len(), 0); } -} \ No newline at end of file +} diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index 41578d44..5f0ff4e4 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -11,6 +11,7 @@ use crate::instance_v2::InstanceBox; use crate::parser::ParseError; use super::BuiltinStdlib; use crate::runtime::{NyashRuntime, NyashRuntimeBuilder}; +use crate::box_factory::BoxFactory; use std::sync::{Arc, Mutex, RwLock}; use std::collections::{HashMap, HashSet}; use thiserror::Error; @@ -343,6 +344,14 @@ impl NyashInterpreter { debug_log("=== NYASH EXECUTION END ==="); result } + + /// Register an additional BoxFactory into this interpreter's runtime registry. + /// This allows tests or embedders to inject custom factories without globals. + pub fn register_box_factory(&mut self, factory: Arc) { + if let Ok(mut reg) = self.runtime.box_registry.lock() { + reg.register(factory); + } + } /// ノードを実行 fn execute_node(&mut self, node: &ASTNode) -> Result, RuntimeError> { diff --git a/src/interpreter/objects.rs b/src/interpreter/objects.rs index d614b6c5..68c9b4f0 100644 --- a/src/interpreter/objects.rs +++ b/src/interpreter/objects.rs @@ -1077,30 +1077,11 @@ impl NyashInterpreter { /// 型が有効かどうかをチェック fn is_valid_type(&self, type_name: &str) -> bool { - // 基本的なビルトイン型 - let is_builtin = matches!(type_name, - "IntegerBox" | "StringBox" | "BoolBox" | "ArrayBox" | "MapBox" | - "FileBox" | "ResultBox" | "FutureBox" | "ChannelBox" | "MathBox" | - "TimeBox" | "DateTimeBox" | "TimerBox" | "RandomBox" | "SoundBox" | - "DebugBox" | "MethodBox" | "NullBox" | "ConsoleBox" | "FloatBox" | - "BufferBox" | "RegexBox" | "JSONBox" | "StreamBox" | "HTTPClientBox" | - "IntentBox" | "P2PBox" - ); - - // Web専用Box(WASM環境のみ) - #[cfg(target_arch = "wasm32")] - let is_web_box = matches!(type_name, "WebDisplayBox" | "WebConsoleBox" | "WebCanvasBox"); - #[cfg(not(target_arch = "wasm32"))] - let is_web_box = false; - - // GUI専用Box(非WASM環境のみ) - #[cfg(all(feature = "gui", not(target_arch = "wasm32")))] - let is_gui_box = matches!(type_name, "EguiBox"); - #[cfg(not(all(feature = "gui", not(target_arch = "wasm32"))))] - let is_gui_box = false; - - is_builtin || is_web_box || is_gui_box || - // または登録済みのユーザー定義Box + // Check unified registry for builtin/plugin/user factories + if let Ok(reg) = self.runtime.box_registry.lock() { + if reg.has_type(type_name) { return true; } + } + // Or user-declared boxes in current program self.shared.box_declarations.read().unwrap().contains_key(type_name) } diff --git a/tests/e2e_plugin_echo.rs b/tests/e2e_plugin_echo.rs new file mode 100644 index 00000000..eab0922e --- /dev/null +++ b/tests/e2e_plugin_echo.rs @@ -0,0 +1,116 @@ +//! E2E test for unified registry with a mock plugin factory +use std::sync::Arc; + +use nyash_rust::box_factory::BoxFactory; +use nyash_rust::box_factory::builtin::BuiltinGroups; +use nyash_rust::interpreter::{NyashInterpreter, SharedState, RuntimeError}; +use nyash_rust::runtime::NyashRuntimeBuilder; +use nyash_rust::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox}; + +// ---------- Mock plugin boxes ---------- + +#[derive(Debug, Clone)] +struct EchoBox { base: BoxBase, msg: String } + +impl EchoBox { fn new(msg: String) -> Self { Self { base: BoxBase::new(), msg } } } + +impl BoxCore for EchoBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { None } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "EchoBox(\"{}\")", self.msg) + } + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } +} + +impl NyashBox for EchoBox { + fn to_string_box(&self) -> StringBox { StringBox::new(self.msg.clone()) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { + if let Some(e) = other.as_any().downcast_ref::() { BoolBox::new(self.msg == e.msg) } else { BoolBox::new(false) } + } + fn type_name(&self) -> &'static str { "EchoBox" } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { Box::new(self.clone()) } +} + +#[derive(Debug, Clone)] +struct AdderBox { base: BoxBase, sum: i64 } +impl AdderBox { fn new(a: i64, b: i64) -> Self { Self { base: BoxBase::new(), sum: a + b } } } + +impl BoxCore for AdderBox { + fn box_id(&self) -> u64 { self.base.id } + fn parent_type_id(&self) -> Option { None } + fn fmt_box(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "AdderBox(sum={})", self.sum) } + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } +} + +impl NyashBox for AdderBox { + fn to_string_box(&self) -> StringBox { StringBox::new(self.sum.to_string()) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { + if let Some(a) = other.as_any().downcast_ref::() { BoolBox::new(self.sum == a.sum) } else { BoolBox::new(false) } + } + fn type_name(&self) -> &'static str { "AdderBox" } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { Box::new(self.clone()) } +} + +// ---------- Mock plugin factory ---------- + +struct TestPluginFactory; +impl TestPluginFactory { fn new() -> Self { Self } } + +impl BoxFactory for TestPluginFactory { + fn create_box(&self, name: &str, args: &[Box]) -> Result, RuntimeError> { + match name { + "EchoBox" => { + let msg = args.get(0).map(|a| a.to_string_box().value).unwrap_or_else(|| "".to_string()); + Ok(Box::new(EchoBox::new(msg))) + } + "AdderBox" => { + if args.len() != 2 { return Err(RuntimeError::InvalidOperation{ message: format!("AdderBox expects 2 args, got {}", args.len()) }); } + let a = args[0].to_string_box().value.parse::().map_err(|_| RuntimeError::TypeError{ message: "AdderBox arg a must be int".into() })?; + let b = args[1].to_string_box().value.parse::().map_err(|_| RuntimeError::TypeError{ message: "AdderBox arg b must be int".into() })?; + Ok(Box::new(AdderBox::new(a, b))) + } + _ => Err(RuntimeError::InvalidOperation{ message: format!("Unknown Box type: {}", name) }) + } + } + + fn box_types(&self) -> Vec<&str> { vec!["EchoBox", "AdderBox"] } +} + +// ---------- E2E tests ---------- + +fn build_interpreter_with_test_plugin() -> NyashInterpreter { + // Start with a standard interpreter (native_full) + let mut interp = NyashInterpreter::new_with_groups(BuiltinGroups::native_full()); + // Inject our mock plugin factory into the interpreter's private registry + interp.register_box_factory(Arc::new(TestPluginFactory::new())); + interp +} + +#[test] +fn e2e_create_echo_box_and_return_string() { + let mut i = build_interpreter_with_test_plugin(); + let code = r#" + e = new EchoBox("hi") + e + "#; + let ast = nyash_rust::parser::NyashParser::parse_from_string(code).expect("parse ok"); + let result = i.execute(ast).expect("exec ok"); + assert_eq!(result.to_string_box().value, "hi"); +} + +#[test] +fn e2e_create_adder_box_and_return_sum() { + let mut i = build_interpreter_with_test_plugin(); + let code = r#" + a = new AdderBox(10, 32) + a + "#; + let ast = nyash_rust::parser::NyashParser::parse_from_string(code).expect("parse ok"); + let result = i.execute(ast).expect("exec ok"); + assert_eq!(result.to_string_box().value, "42"); +}