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:
Moe Charm
2025-08-21 15:30:57 +09:00
parent 2fc6ce3aa6
commit 779e3be5d9
5 changed files with 191 additions and 27 deletions

View File

@ -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

View File

@ -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);
@ -114,6 +150,26 @@ impl UnifiedBoxRegistry {
})
}
/// 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<String> {
let mut types = Vec::new();

View File

@ -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;
@ -344,6 +345,14 @@ impl NyashInterpreter {
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> {
match node {

View File

@ -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専用BoxWASM環境のみ
#[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)
}

116
tests/e2e_plugin_echo.rs Normal file
View 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");
}