diff --git a/docs/development/roadmap/idea/README.md b/docs/development/roadmap/idea/README.md new file mode 100644 index 00000000..ad0d2c91 --- /dev/null +++ b/docs/development/roadmap/idea/README.md @@ -0,0 +1,51 @@ +Nyash Dev Ideas / Roadmap Scratchpad + +Purpose +- Quick scratchpad to capture ideas before they get shaped into tasks/PRs. +- Keep entries short and action-oriented; promote to RFC/docs when stable. + +Near-Term (High Impact) +- Strengthen plugin ABI tester: + - Parse `nyash.toml` and verify method_id wiring with minimal invokes (birth + no-arg methods). + - Add contract tests for net plugin boxes (HttpRequest/Response/Client/Server). + - Fail fast with clear diffs when IDs drift. +- Make debug logs opt-in (done): + - `NYASH_DEBUG_PLUGIN=1` gates VM→Plugin bridge logs. + - `NYASH_NET_LOG=1` gates net plugin logs. +- Regression tests: + - Minimal end-to-end for HTTP: get→accept→respond→readBody (body non-empty). + - ABI sanity tests per box type (already added for HttpRequestBox). + +Identity & Copy Semantics +- Enforce `is_identity()` and `clone_or_share()` usage across interpreter paths. +- Audit remaining `clone_box()` callsites; switch to `clone_or_share()` where identity is required. +- Optional: add dev assertions when PluginBoxV2 instance_id changes unexpectedly in a single flow. + +Single Source of Truth for Method IDs +- Avoid hand-edit drift between plugin code and `nyash.toml`: + - Option A: Generate `nyash.toml` from Rust consts (build.rs or small generator). + - Option B: Generate Rust method_id consts from `nyash.toml`. + - Start with a checker that diffs the two (cheap safety net). + +CI / Tooling +- Add `plugin_contract_*` tests to default CI. +- Add a `--ci-quiet` profile to suppress debug logs; emit logs only on failure. +- Introduce a small utility to reserve/randomize test ports to avoid collisions. + +Net Plugin Polishing +- Keep TCP-only path; prefer accepted TCP requests in server.accept (done). +- Mirror response body into client handle using X-Nyash-Resp-Id (done); document behavior. +- Add timeouts and clearer error messages for readBody stalls. + +Docs +- Expand `docs/development/box_identity_and_copy_semantics.md` with examples and anti-patterns. +- Add a quickstart: “How to debug the net plugin” (env flags, key logs, typical pitfalls). + +Future Ideas +- Lightweight codegen for plugin schemas (IDs, arg signatures, returns_result) → toml + Rust. +- Lint/pass that forbids `clone_box()` on identity boxes in critical paths. +- Structured logging with categories and levels (trace/info/warn) pluggable to CI. + +Notes +- Keep this file as a living list; prune as items graduate to tracked issues/PRs. + diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 42c6990b..464ea63a 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -15,6 +15,7 @@ mod enabled { use std::any::Any; use once_cell::sync::Lazy; use crate::runtime::leak_tracker; + fn dbg_on() -> bool { std::env::var("NYASH_DEBUG_PLUGIN").unwrap_or_default() == "1" } /// Loaded plugin information pub struct LoadedPluginV2 { @@ -131,7 +132,7 @@ mod enabled { } fn clone_box(&self) -> Box { - eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.inner.instance_id); + if dbg_on() { eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.inner.instance_id); } // Clone means creating a new instance by calling birth() on the plugin let mut output_buffer = vec![0u8; 1024]; let mut output_len = output_buffer.len(); @@ -153,7 +154,7 @@ mod enabled { let new_instance_id = u32::from_le_bytes([ output_buffer[0], output_buffer[1], output_buffer[2], output_buffer[3] ]); - eprintln!("🎉 clone_box success: created new {} instance_id={}", self.box_type, new_instance_id); + if dbg_on() { eprintln!("🎉 clone_box success: created new {} instance_id={}", self.box_type, new_instance_id); } Box::new(PluginBoxV2 { box_type: self.box_type.clone(), inner: std::sync::Arc::new(PluginHandleInner { @@ -165,7 +166,7 @@ mod enabled { }), }) } else { - eprintln!("❌ clone_box failed: birth() returned error code {}", result); + if dbg_on() { eprintln!("❌ clone_box failed: birth() returned error code {}", result); } Box::new(StringBox::new(format!("Clone failed for {}", self.box_type))) } } @@ -179,7 +180,7 @@ mod enabled { } fn share_box(&self) -> Box { - eprintln!("🔍 DEBUG: PluginBoxV2::share_box called for {} (id={})", self.box_type, self.inner.instance_id); + if dbg_on() { eprintln!("🔍 DEBUG: PluginBoxV2::share_box called for {} (id={})", self.box_type, self.inner.instance_id); } // Share means returning a new Box with the same instance_id Box::new(PluginBoxV2 { @@ -480,6 +481,7 @@ impl PluginBoxV2 { buf }; eprintln!("[VM→Plugin] call {}.{} recv_id={} returns_result={}", box_type, method_name, instance_id, returns_result); + if dbg_on() { eprintln!("[VM→Plugin] call {}.{} recv_id={} returns_result={}", box_type, method_name, instance_id, returns_result); } let mut out = vec![0u8; 1024]; let mut out_len: usize = out.len(); let rc = unsafe { @@ -495,7 +497,7 @@ impl PluginBoxV2 { }; if rc != 0 { let be = BidError::from_raw(rc); - eprintln!("[PluginLoaderV2] invoke rc={} ({}) for {}.{}", rc, be.message(), box_type, method_name); + if dbg_on() { eprintln!("[PluginLoaderV2] invoke rc={} ({}) for {}.{}", rc, be.message(), box_type, method_name); } if returns_result { let err = crate::exception_box::ErrorBox::new(&format!("{} (code: {})", be.message(), rc)); return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(err))))); @@ -531,7 +533,7 @@ impl PluginBoxV2 { let mut i = [0u8;4]; i.copy_from_slice(&payload[4..8]); let r_type = u32::from_le_bytes(t); let r_inst = u32::from_le_bytes(i); - eprintln!("[Plugin→VM] return handle type_id={} inst={} (returns_result={})", r_type, r_inst, returns_result); + if dbg_on() { eprintln!("[Plugin→VM] return handle type_id={} inst={} (returns_result={})", r_type, r_inst, returns_result); } // Map type_id -> (lib_name, box_name) if let Some((ret_lib, ret_box)) = self.find_box_by_type_id(config, &toml_value, r_type) { // Get plugin for ret_lib @@ -564,17 +566,17 @@ impl PluginBoxV2 { 2 if size == 4 => { // I32 let mut b = [0u8;4]; b.copy_from_slice(payload); let val: Box = Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64)); - eprintln!("[Plugin→VM] return i32 value={} (returns_result={})", i32::from_le_bytes(b), returns_result); + if dbg_on() { eprintln!("[Plugin→VM] return i32 value={} (returns_result={})", i32::from_le_bytes(b), returns_result); } if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box) } else { Some(val) } } 6 | 7 => { // String/Bytes let s = String::from_utf8_lossy(payload).to_string(); let val: Box = Box::new(StringBox::new(s)); - eprintln!("[Plugin→VM] return str/bytes len={} (returns_result={})", size, returns_result); + if dbg_on() { eprintln!("[Plugin→VM] return str/bytes len={} (returns_result={})", size, returns_result); } if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box) } else { Some(val) } } 9 => { - eprintln!("[Plugin→VM] return void (returns_result={})", returns_result); + if dbg_on() { eprintln!("[Plugin→VM] return void (returns_result={})", returns_result); } if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box) } else { None } }, _ => None, diff --git a/tests/plugin_contract_net_ids.rs b/tests/plugin_contract_net_ids.rs new file mode 100644 index 00000000..b87a630f --- /dev/null +++ b/tests/plugin_contract_net_ids.rs @@ -0,0 +1,39 @@ +#![cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + +use nyash_rust::parser::NyashParser; +use nyash_rust::runtime::plugin_loader_v2::{init_global_loader_v2, get_global_loader_v2}; +use nyash_rust::runtime::box_registry::get_global_registry; +use nyash_rust::runtime::PluginConfig; + +fn try_init_plugins() -> bool { + if !std::path::Path::new("nyash.toml").exists() { return false; } + if let Err(e) = init_global_loader_v2("nyash.toml") { eprintln!("init failed: {:?}", e); return false; } + let loader = get_global_loader_v2(); + let loader = loader.read().unwrap(); + if let Some(conf) = &loader.config { + let mut map = std::collections::HashMap::new(); + for (lib, def) in &conf.libraries { for b in &def.boxes { map.insert(b.clone(), lib.clone()); } } + get_global_registry().apply_plugin_config(&PluginConfig { plugins: map }); + true + } else { false } +} + +/// Minimal ABI sanity check: HttpRequestBox.path=1, readBody=2 +#[test] +fn plugin_contract_http_request_ids_sanity() { + if !try_init_plugins() { return; } + // Exercise HttpRequestBox.path/readBody on a birthed request (no server needed) + let code = r#" +local req, p, b +req = new HttpRequestBox() +p = req.path() +b = req.readBody() +p + ":" + b +"#; + let ast = NyashParser::parse_from_string(code).expect("parse failed"); + let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new(); + let result = interpreter.execute(ast).expect("exec failed"); + // Default path="", body="" for birthed request + assert_eq!(result.to_string_box().value, ":"); +} +