feat: Major documentation reorganization and unified Box design updates
## Documentation & Organization - Moved copilot_issues.txt → 00_MASTER_ROADMAP.md (phases folder) - Created Phase 9.79b.1 & 9.79b.2 plans for unified Box implementation - Updated unified-box-design-deep-analysis.md with ChatGPT5 insights - Added P2P documentation and examples (ping-pong, self-ping) ## Code Updates - P2PBox: Reverted to original error state for demonstration - VM: Enhanced BoxCall dispatch for unified approach - Updated box factory, interpreter calls, and transport layer ## Cleanup & Privacy - Removed private/ and private_test/ from git tracking - Added private folders to .gitignore for security - Cleaned root directory: moved backups, removed temp files - Moved consultation files to docs/archive/consultations/ ## Other Improvements - Added object literal syntax improvement idea - Updated CLAUDE.md with master roadmap reference - Updated CURRENT_TASK.md with latest progress 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -539,10 +539,41 @@ impl VM {
|
||||
}
|
||||
|
||||
/// Call a method on a Box - simplified version of interpreter method dispatch
|
||||
pub(super) fn call_box_method(&self, box_value: Box<dyn NyashBox>, method: &str, _args: Vec<Box<dyn NyashBox>>) -> Result<Box<dyn NyashBox>, VMError> {
|
||||
pub(super) fn call_box_method(&self, box_value: Box<dyn NyashBox>, method: &str, mut _args: Vec<Box<dyn NyashBox>>) -> Result<Box<dyn NyashBox>, VMError> {
|
||||
// For now, implement basic methods for common box types
|
||||
// This is a simplified version - real implementation would need full method dispatch
|
||||
|
||||
// 🌟 Universal methods pre-dispatch (non-invasive)
|
||||
match method {
|
||||
"toString" => {
|
||||
if !_args.is_empty() {
|
||||
return Ok(Box::new(StringBox::new(format!("Error: toString() expects 0 arguments, got {}", _args.len()))));
|
||||
}
|
||||
return Ok(Box::new(StringBox::new(box_value.to_string_box().value)));
|
||||
}
|
||||
"type" => {
|
||||
if !_args.is_empty() {
|
||||
return Ok(Box::new(StringBox::new(format!("Error: type() expects 0 arguments, got {}", _args.len()))));
|
||||
}
|
||||
return Ok(Box::new(StringBox::new(box_value.type_name())));
|
||||
}
|
||||
"equals" => {
|
||||
if _args.len() != 1 {
|
||||
return Ok(Box::new(StringBox::new(format!("Error: equals() expects 1 argument, got {}", _args.len()))));
|
||||
}
|
||||
let rhs = _args.remove(0);
|
||||
let eq = box_value.equals(&*rhs);
|
||||
return Ok(Box::new(eq));
|
||||
}
|
||||
"clone" => {
|
||||
if !_args.is_empty() {
|
||||
return Ok(Box::new(StringBox::new(format!("Error: clone() expects 0 arguments, got {}", _args.len()))));
|
||||
}
|
||||
return Ok(box_value.clone_box());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ResultBox (NyashResultBox - new)
|
||||
if let Some(result_box) = box_value.as_any().downcast_ref::<crate::boxes::result::NyashResultBox>() {
|
||||
match method {
|
||||
@ -562,11 +593,6 @@ impl VM {
|
||||
|
||||
// Legacy box_trait::ResultBox is no longer handled here (migration complete)
|
||||
|
||||
// Generic fallback: toString for any Box type
|
||||
if method == "toString" {
|
||||
return Ok(Box::new(StringBox::new(box_value.to_string_box().value)));
|
||||
}
|
||||
|
||||
// StringBox methods
|
||||
if let Some(string_box) = box_value.as_any().downcast_ref::<StringBox>() {
|
||||
match method {
|
||||
|
||||
@ -455,8 +455,15 @@ impl BuiltinBoxFactory {
|
||||
});
|
||||
}
|
||||
let name = args[0].to_string_box().value;
|
||||
// Accept multiple payload forms: JSON string, JSONBox, MapBox
|
||||
let payload_str = if let Some(jb) = args[1].as_any().downcast_ref::<crate::boxes::json::JSONBox>() {
|
||||
jb.to_string()
|
||||
} else if let Some(mb) = args[1].as_any().downcast_ref::<crate::boxes::map_box::MapBox>() {
|
||||
mb.toJSON().to_string_box().value
|
||||
} else {
|
||||
args[1].to_string_box().value
|
||||
};
|
||||
// Try parse payload as JSON, fallback to string
|
||||
let payload_str = args[1].to_string_box().value;
|
||||
let payload = match serde_json::from_str::<serde_json::Value>(&payload_str) {
|
||||
Ok(json) => json,
|
||||
Err(_) => serde_json::Value::String(payload_str),
|
||||
|
||||
@ -179,9 +179,15 @@ impl P2PBox {
|
||||
if let Some(method_box) = handler.as_any().downcast_ref::<MethodBox>() {
|
||||
let method_clone = method_box.clone();
|
||||
let intent_name = intent_str.to_string();
|
||||
// capture state holders for receive-side tracing
|
||||
let last_from = Arc::clone(&self.last_from);
|
||||
let last_intent = Arc::clone(&self.last_intent_name);
|
||||
t.register_intent_handler(&intent_name, Box::new(move |env| {
|
||||
// flagがtrueのときのみ実行
|
||||
if flag.load(Ordering::SeqCst) {
|
||||
// Update receive-side traces for E2E visibility
|
||||
if let Ok(mut lf) = last_from.write() { *lf = Some(env.from.clone()); }
|
||||
if let Ok(mut li) = last_intent.write() { *li = Some(env.intent.get_name().to_string_box().value); }
|
||||
let _ = method_clone.invoke(vec![
|
||||
Box::new(env.intent.clone()),
|
||||
Box::new(StringBox::new(env.from.clone())),
|
||||
@ -253,6 +259,16 @@ impl P2PBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// デバッグ: intentに対する有効ハンドラー数(trueフラグ数)
|
||||
pub fn debug_active_handler_count(&self, intent_name: Box<dyn NyashBox>) -> Box<dyn NyashBox> {
|
||||
let name = intent_name.to_string_box().value;
|
||||
let flags = self.handler_flags.read().unwrap();
|
||||
let cnt = flags.get(&name)
|
||||
.map(|v| v.iter().filter(|f| f.load(Ordering::SeqCst)).count())
|
||||
.unwrap_or(0);
|
||||
Box::new(crate::box_trait::IntegerBox::new(cnt as i64))
|
||||
}
|
||||
|
||||
/// 最後に受信したfromを取得(ループバック検証用)
|
||||
pub fn get_last_from(&self) -> Box<dyn NyashBox> {
|
||||
let v = self.last_from.read().unwrap().clone().unwrap_or_default();
|
||||
@ -354,4 +370,83 @@ mod tests {
|
||||
assert_eq!(p.get_last_from().to_string_box().value, "alice".to_string());
|
||||
assert_eq!(p.get_last_intent_name().to_string_box().value, "ping".to_string());
|
||||
}
|
||||
|
||||
/// Internal helper for tests: register raw Rust handler with optional async reply
|
||||
impl P2PBox {
|
||||
#[allow(dead_code)]
|
||||
fn __debug_on_rust(&self, intent: &str, reply_intent: Option<&str>) {
|
||||
if let Ok(mut t) = self.transport.write() {
|
||||
let intent_name = intent.to_string();
|
||||
let last_from = Arc::clone(&self.last_from);
|
||||
let last_intent = Arc::clone(&self.last_intent_name);
|
||||
// create self clone for reply
|
||||
let self_clone = self.clone();
|
||||
let reply_name = reply_intent.map(|s| s.to_string());
|
||||
t.register_intent_handler(&intent_name, Box::new(move |env| {
|
||||
if let Ok(mut lf) = last_from.write() { *lf = Some(env.from.clone()); }
|
||||
if let Ok(mut li) = last_intent.write() { *li = Some(env.intent.get_name().to_string_box().value); }
|
||||
if let Some(rn) = reply_name.clone() {
|
||||
let to = env.from.clone();
|
||||
std::thread::spawn(move || {
|
||||
// slight delay to avoid lock contention
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
let intent = IntentBox::new(rn, serde_json::json!({}));
|
||||
let _ = self_clone.send(Box::new(StringBox::new(to)), Box::new(intent));
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_node_ping_pong() {
|
||||
let alice = P2PBox::new("alice".to_string(), TransportKind::InProcess);
|
||||
let bob = P2PBox::new("bob".to_string(), TransportKind::InProcess);
|
||||
// bob replies pong to ping
|
||||
bob.__debug_on_rust("ping", Some("pong"));
|
||||
// alice listens pong
|
||||
alice.__debug_on_rust("pong", None);
|
||||
// send ping
|
||||
let ping = IntentBox::new("ping".to_string(), serde_json::json!({}));
|
||||
let _ = alice.send(Box::new(StringBox::new("bob")), Box::new(ping));
|
||||
// bob should record ping
|
||||
assert_eq!(bob.get_last_intent_name().to_string_box().value, "ping");
|
||||
// allow async reply
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
// alice should record pong
|
||||
assert_eq!(alice.get_last_intent_name().to_string_box().value, "pong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_once_disables_after_first_delivery() {
|
||||
let p = P2PBox::new("alice".to_string(), TransportKind::InProcess);
|
||||
// Register one-time handler for 'hello'
|
||||
let handler = crate::method_box::MethodBox::new(Box::new(p.clone()), "noop".to_string());
|
||||
let _ = p.on_once(Box::new(StringBox::new("hello")), Box::new(handler));
|
||||
// Initially active = 1
|
||||
let c0 = p.debug_active_handler_count(Box::new(StringBox::new("hello")));
|
||||
assert_eq!(c0.to_string_box().value, "1");
|
||||
// Send twice to self
|
||||
let intent = IntentBox::new("hello".to_string(), serde_json::json!({}));
|
||||
let _ = p.send(Box::new(StringBox::new("alice")), Box::new(intent.clone()));
|
||||
let _ = p.send(Box::new(StringBox::new("alice")), Box::new(intent));
|
||||
// After first delivery, once-flag should be false => active count = 0
|
||||
let c1 = p.debug_active_handler_count(Box::new(StringBox::new("hello")));
|
||||
assert_eq!(c1.to_string_box().value, "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_clears_handlers() {
|
||||
let p = P2PBox::new("bob".to_string(), TransportKind::InProcess);
|
||||
let handler = crate::method_box::MethodBox::new(Box::new(p.clone()), "noop".to_string());
|
||||
let _ = p.on(Box::new(StringBox::new("bye")), Box::new(handler));
|
||||
// Active = 1
|
||||
let c0 = p.debug_active_handler_count(Box::new(StringBox::new("bye")));
|
||||
assert_eq!(c0.to_string_box().value, "1");
|
||||
// Off
|
||||
let _ = p.off(Box::new(StringBox::new("bye")));
|
||||
let c1 = p.debug_active_handler_count(Box::new(StringBox::new("bye")));
|
||||
assert_eq!(c1.to_string_box().value, "0");
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,6 +229,38 @@ impl NyashInterpreter {
|
||||
// オブジェクトを評価(通常のメソッド呼び出し)
|
||||
let obj_value = self.execute_expression(object)?;
|
||||
idebug!("🔍 DEBUG: execute_method_call - object type: {}, method: {}", obj_value.type_name(), method);
|
||||
|
||||
// 🌟 ユニバーサルメソッド前段ディスパッチ(非侵襲)
|
||||
// toString()/type()/equals(x)/clone() をトレイトに直結
|
||||
match method {
|
||||
"toString" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation { message: format!("toString() expects 0 arguments, got {}", arguments.len()) });
|
||||
}
|
||||
return Ok(Box::new(obj_value.to_string_box()));
|
||||
}
|
||||
"type" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation { message: format!("type() expects 0 arguments, got {}", arguments.len()) });
|
||||
}
|
||||
return Ok(Box::new(StringBox::new(obj_value.type_name())));
|
||||
}
|
||||
"equals" => {
|
||||
if arguments.len() != 1 {
|
||||
return Err(RuntimeError::InvalidOperation { message: format!("equals() expects 1 argument, got {}", arguments.len()) });
|
||||
}
|
||||
let rhs = self.execute_expression(&arguments[0])?;
|
||||
let eq = obj_value.equals(&*rhs);
|
||||
return Ok(Box::new(eq));
|
||||
}
|
||||
"clone" => {
|
||||
if !arguments.is_empty() {
|
||||
return Err(RuntimeError::InvalidOperation { message: format!("clone() expects 0 arguments, got {}", arguments.len()) });
|
||||
}
|
||||
return Ok(obj_value.clone_box());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Builtin dispatch (centralized)
|
||||
if let Some(res) = self.dispatch_builtin_method(&obj_value, method, arguments) {
|
||||
|
||||
@ -47,6 +47,9 @@ impl NyashInterpreter {
|
||||
method: &str,
|
||||
arguments: &[ASTNode],
|
||||
) -> Result<Box<dyn NyashBox>, RuntimeError> {
|
||||
if crate::interpreter::utils::debug_on() || std::env::var("NYASH_DEBUG_P2P").unwrap_or_default() == "1" {
|
||||
eprintln!("[Interp:P2P] {}(..) called with {} args", method, arguments.len());
|
||||
}
|
||||
match method {
|
||||
// ノードID取得
|
||||
"getNodeId" | "getId" => Ok(p2p_box.get_node_id()),
|
||||
@ -74,7 +77,7 @@ impl NyashInterpreter {
|
||||
}
|
||||
|
||||
// on メソッド実装(ResultBox返却)
|
||||
, "on" => {
|
||||
"on" => {
|
||||
if arguments.len() < 2 {
|
||||
return Err(RuntimeError::InvalidOperation { message: "on requires (intentName, handler) arguments".to_string() });
|
||||
}
|
||||
|
||||
@ -114,6 +114,9 @@ impl MessageBusData {
|
||||
/// メッセージをルーティング
|
||||
pub fn route(&self, to: &str, intent: IntentBox, from: &str) -> Result<(), SendError> {
|
||||
if let Some(endpoint) = self.nodes.get(to) {
|
||||
if std::env::var("NYASH_DEBUG_P2P").unwrap_or_default() == "1" {
|
||||
eprintln!("[MessageBus] route {} -> {} intent={}", from, to, intent.get_name().to_string_box().value);
|
||||
}
|
||||
endpoint.deliver(intent, from);
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
@ -109,6 +109,9 @@ impl Transport for InProcessTransport {
|
||||
let cb = std::sync::Arc::new(cb);
|
||||
let cb_clone = cb.clone();
|
||||
// Adapt to MessageBus handler signature
|
||||
if std::env::var("NYASH_DEBUG_P2P").unwrap_or_default() == "1" {
|
||||
eprintln!("[InProcessTransport] register handler node={} intent={}", self.node_id, intent_name);
|
||||
}
|
||||
self.add_handler(&intent_name, Box::new(move |intent_box: IntentBox, from: &str| {
|
||||
let env = IntentEnvelope {
|
||||
from: from.to_string(),
|
||||
@ -133,7 +136,17 @@ impl Transport for InProcessTransport {
|
||||
|
||||
impl Drop for InProcessTransport {
|
||||
fn drop(&mut self) {
|
||||
// NOTE: Temporarily disabled unregister to avoid interfering with shared-node lifetimes.
|
||||
// Proper refcounted unregister will be implemented later.
|
||||
// Safe unregister: only remove if the current endpoint matches the registry entry
|
||||
if let Ok(mut bus) = self.bus.lock() {
|
||||
let removed = bus.unregister_if_same(&self.node_id, &self.endpoint);
|
||||
if std::env::var("NYASH_DEBUG_P2P").unwrap_or_default() == "1" {
|
||||
eprintln!(
|
||||
"[InProcessTransport::drop] node_id={} removed={} (bus={:?})",
|
||||
self.node_id,
|
||||
removed,
|
||||
&*bus
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user