fix: Unified registry e2e tests passing and Nyash code fix
- Added is_builtin_factory() method to BoxFactory trait - Added register_box_factory() method to NyashInterpreter for dynamic factory registration - Added is_valid_type() and has_type() methods for unified type checking - Fixed e2e tests by removing 'return' statements (Nyash doesn't require them at top level) - Created TestPluginFactory with EchoBox and AdderBox for testing - All e2e tests now passing: EchoBox returns "hi", AdderBox returns "42" This commit finalizes the unified registry system with full support for dynamic factory registration, enabling plugins and custom Box types to be seamlessly integrated into the interpreter at runtime. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -581,6 +581,8 @@ impl BoxFactory for BuiltinBoxFactory {
|
|||||||
fn box_types(&self) -> Vec<&str> {
|
fn box_types(&self) -> Vec<&str> {
|
||||||
self.creators.keys().map(|s| s.as_str()).collect()
|
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
|
/// Declarative macro for registering multiple Box types at once
|
||||||
|
|||||||
@ -36,6 +36,11 @@ pub trait BoxFactory: Send + Sync {
|
|||||||
fn supports_birth(&self) -> bool {
|
fn supports_birth(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identify builtin factory to enforce reserved-name protections
|
||||||
|
fn is_builtin_factory(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registry that manages all BoxFactory implementations
|
/// Registry that manages all BoxFactory implementations
|
||||||
@ -64,10 +69,41 @@ impl UnifiedBoxRegistry {
|
|||||||
|
|
||||||
// Update cache
|
// Update cache
|
||||||
let mut cache = self.type_cache.write().unwrap();
|
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 {
|
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)
|
// First registered factory wins (priority order)
|
||||||
cache.entry(type_name.to_string())
|
let entry = cache.entry(type_name.to_string());
|
||||||
.or_insert(factory_index);
|
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);
|
self.factories.push(factory);
|
||||||
@ -113,6 +149,26 @@ impl UnifiedBoxRegistry {
|
|||||||
message: format!("Unknown Box type: {}", name),
|
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
|
/// Get all available Box types
|
||||||
pub fn available_types(&self) -> Vec<String> {
|
pub fn available_types(&self) -> Vec<String> {
|
||||||
@ -144,4 +200,4 @@ mod tests {
|
|||||||
let registry = UnifiedBoxRegistry::new();
|
let registry = UnifiedBoxRegistry::new();
|
||||||
assert_eq!(registry.available_types().len(), 0);
|
assert_eq!(registry.available_types().len(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use crate::instance_v2::InstanceBox;
|
|||||||
use crate::parser::ParseError;
|
use crate::parser::ParseError;
|
||||||
use super::BuiltinStdlib;
|
use super::BuiltinStdlib;
|
||||||
use crate::runtime::{NyashRuntime, NyashRuntimeBuilder};
|
use crate::runtime::{NyashRuntime, NyashRuntimeBuilder};
|
||||||
|
use crate::box_factory::BoxFactory;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -343,6 +344,14 @@ impl NyashInterpreter {
|
|||||||
debug_log("=== NYASH EXECUTION END ===");
|
debug_log("=== NYASH EXECUTION END ===");
|
||||||
result
|
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<dyn BoxFactory>) {
|
||||||
|
if let Ok(mut reg) = self.runtime.box_registry.lock() {
|
||||||
|
reg.register(factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ノードを実行
|
/// ノードを実行
|
||||||
fn execute_node(&mut self, node: &ASTNode) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
fn execute_node(&mut self, node: &ASTNode) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||||
|
|||||||
@ -1077,30 +1077,11 @@ impl NyashInterpreter {
|
|||||||
|
|
||||||
/// 型が有効かどうかをチェック
|
/// 型が有効かどうかをチェック
|
||||||
fn is_valid_type(&self, type_name: &str) -> bool {
|
fn is_valid_type(&self, type_name: &str) -> bool {
|
||||||
// 基本的なビルトイン型
|
// Check unified registry for builtin/plugin/user factories
|
||||||
let is_builtin = matches!(type_name,
|
if let Ok(reg) = self.runtime.box_registry.lock() {
|
||||||
"IntegerBox" | "StringBox" | "BoolBox" | "ArrayBox" | "MapBox" |
|
if reg.has_type(type_name) { return true; }
|
||||||
"FileBox" | "ResultBox" | "FutureBox" | "ChannelBox" | "MathBox" |
|
}
|
||||||
"TimeBox" | "DateTimeBox" | "TimerBox" | "RandomBox" | "SoundBox" |
|
// Or user-declared boxes in current program
|
||||||
"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
|
|
||||||
self.shared.box_declarations.read().unwrap().contains_key(type_name)
|
self.shared.box_declarations.read().unwrap().contains_key(type_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
tests/e2e_plugin_echo.rs
Normal file
116
tests/e2e_plugin_echo.rs
Normal file
@ -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<std::any::TypeId> { 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::<EchoBox>() { BoolBox::new(self.msg == e.msg) } else { BoolBox::new(false) }
|
||||||
|
}
|
||||||
|
fn type_name(&self) -> &'static str { "EchoBox" }
|
||||||
|
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(self.clone()) }
|
||||||
|
fn share_box(&self) -> Box<dyn NyashBox> { 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<std::any::TypeId> { 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::<AdderBox>() { BoolBox::new(self.sum == a.sum) } else { BoolBox::new(false) }
|
||||||
|
}
|
||||||
|
fn type_name(&self) -> &'static str { "AdderBox" }
|
||||||
|
fn clone_box(&self) -> Box<dyn NyashBox> { Box::new(self.clone()) }
|
||||||
|
fn share_box(&self) -> Box<dyn NyashBox> { 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<dyn NyashBox>]) -> Result<Box<dyn NyashBox>, 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::<i64>().map_err(|_| RuntimeError::TypeError{ message: "AdderBox arg a must be int".into() })?;
|
||||||
|
let b = args[1].to_string_box().value.parse::<i64>().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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user