diff --git a/e2e_filebox_test_error.txt b/e2e_filebox_test_error.txt new file mode 100644 index 00000000..b1924634 --- /dev/null +++ b/e2e_filebox_test_error.txt @@ -0,0 +1,19 @@ +FileBox Plugin E2E Test Results: + +Build Status: +- Main build: ✅ Success +- Plugin build: ✅ Success (with 3 warnings) + +Test Results: +- e2e_interpreter_plugin_filebox_close_void: ✅ PASSED +- e2e_vm_plugin_filebox_close_void: ✅ PASSED +- e2e_interpreter_plugin_filebox_delegation: ❌ FAILED + +Failure Details: +❌ Interpreter error: Invalid operation: birth() method not yet implemented for builtin box 'FileBox' + +The error occurs when trying to create a LoggingFileBox that delegates from FileBox. +The test expects to use birth() constructor but FileBox (as a plugin) doesn't implement it yet. + +This suggests the plugin system is working correctly (FileBox is recognized), but the +birth() constructor delegation for plugin boxes needs implementation. \ No newline at end of file diff --git a/e2e_test_error.txt b/e2e_test_error.txt new file mode 100644 index 00000000..52110b0c --- /dev/null +++ b/e2e_test_error.txt @@ -0,0 +1,31 @@ +NEW ERROR AFTER FIX: + +running 2 tests +test e2e_create_echo_box_and_return_string ... FAILED +test e2e_create_adder_box_and_return_sum ... FAILED + +failures: + +---- e2e_create_echo_box_and_return_string stdout ---- +❌ Interpreter error: Return outside of function + +thread 'e2e_create_echo_box_and_return_string' panicked at tests/e2e_plugin_echo.rs:102:33: +exec ok: ReturnOutsideFunction +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +---- e2e_create_adder_box_and_return_sum stdout ---- +❌ Interpreter error: Return outside of function + +thread 'e2e_create_adder_box_and_return_sum' panicked at tests/e2e_plugin_echo.rs:114:33: +exec ok: ReturnOutsideFunction + + +failures: + e2e_create_adder_box_and_return_sum + e2e_create_echo_box_and_return_string + +test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s + +PROGRESS: The Box types (EchoBox, AdderBox) are now recognized! +The error changed from "Unknown Box type" to "Return outside of function". +This suggests the unified registry is working correctly after ChatGPT5's fix. \ No newline at end of file diff --git a/nyash.toml b/nyash.toml index 75d1ffce..ffcf2a25 100644 --- a/nyash.toml +++ b/nyash.toml @@ -7,6 +7,10 @@ boxes = ["FileBox"] path = "./plugins/nyash-filebox-plugin/target/release/libnyash_filebox_plugin.so" +[libraries."libnyash_counter_plugin.so"] +boxes = ["CounterBox"] +path = "./plugins/nyash-counter-plugin/target/release/libnyash_counter_plugin.so" + # 将来の拡張例: # "libnyash_database_plugin.so" = { # boxes = ["PostgreSQLBox", "MySQLBox", "SQLiteBox"], @@ -32,6 +36,15 @@ copyFrom = { method_id = 7, args = [ { kind = "box", category = "plugin" } ] } # v2.2: BoxRef(Handle)を返すメソッド宣言 cloneSelf = { method_id = 8 } +[libraries."libnyash_counter_plugin.so".CounterBox] +type_id = 7 + +[libraries."libnyash_counter_plugin.so".CounterBox.methods] +birth = { method_id = 0 } +inc = { method_id = 1 } +get = { method_id = 2 } +fini = { method_id = 4294967295 } + [plugin_paths] # プラグインの検索パス(デフォルト) search_paths = [ diff --git a/plugins/nyash-counter-plugin/Cargo.toml b/plugins/nyash-counter-plugin/Cargo.toml new file mode 100644 index 00000000..8f986966 --- /dev/null +++ b/plugins/nyash-counter-plugin/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nyash-counter-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +once_cell = "1.20" + +[features] +default = [] + +[profile.release] +lto = true +strip = true +opt-level = "z" + diff --git a/plugins/nyash-counter-plugin/src/lib.rs b/plugins/nyash-counter-plugin/src/lib.rs new file mode 100644 index 00000000..7d6d082c --- /dev/null +++ b/plugins/nyash-counter-plugin/src/lib.rs @@ -0,0 +1,133 @@ +//! Nyash CounterBox Plugin - BID-FFI v1 Implementation + +use std::collections::HashMap; +use std::sync::{Mutex, atomic::{AtomicU32, Ordering}}; + +use once_cell::sync::Lazy; + +// ===== Error Codes (BID-1 alignment) ===== +const NYB_SUCCESS: i32 = 0; +const NYB_E_SHORT_BUFFER: i32 = -1; +const NYB_E_INVALID_TYPE: i32 = -2; +const NYB_E_INVALID_METHOD: i32 = -3; +const NYB_E_INVALID_ARGS: i32 = -4; +const NYB_E_PLUGIN_ERROR: i32 = -5; +const NYB_E_INVALID_HANDLE: i32 = -8; + +// ===== Method IDs ===== +const METHOD_BIRTH: u32 = 0; // constructor +const METHOD_INC: u32 = 1; // increments and returns new count +const METHOD_GET: u32 = 2; // returns current count +const METHOD_FINI: u32 = u32::MAX; // destructor + +// Assign a unique type_id for CounterBox (distinct from FileBox=6) +const TYPE_ID_COUNTER: u32 = 7; + +// ===== Instance state ===== +struct CounterInstance { count: i32 } + +static INSTANCES: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(1); + +#[no_mangle] +pub extern "C" fn nyash_plugin_abi() -> u32 { 1 } + +#[no_mangle] +pub extern "C" fn nyash_plugin_init() -> i32 { NYB_SUCCESS } + +#[no_mangle] +pub extern "C" fn nyash_plugin_invoke( + type_id: u32, + method_id: u32, + instance_id: u32, + _args: *const u8, + _args_len: usize, + result: *mut u8, + result_len: *mut usize, +) -> i32 { + if type_id != TYPE_ID_COUNTER { return NYB_E_INVALID_TYPE; } + + unsafe { + match method_id { + METHOD_BIRTH => { + // Return new instance handle (u32 id) + if result_len.is_null() { return NYB_E_INVALID_ARGS; } + if preflight(result, result_len, 4) { return NYB_E_SHORT_BUFFER; } + let id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + if let Ok(mut map) = INSTANCES.lock() { + map.insert(id, CounterInstance { count: 0 }); + } else { return NYB_E_PLUGIN_ERROR; } + let bytes = id.to_le_bytes(); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), result, 4); + *result_len = 4; + NYB_SUCCESS + } + METHOD_FINI => { + if let Ok(mut map) = INSTANCES.lock() { + map.remove(&instance_id); + NYB_SUCCESS + } else { NYB_E_PLUGIN_ERROR } + } + METHOD_INC => { + // increments and returns new count as I32 TLV + if let Ok(mut map) = INSTANCES.lock() { + if let Some(inst) = map.get_mut(&instance_id) { + inst.count += 1; + let v = inst.count; + if preflight(result, result_len, 12) { return NYB_E_SHORT_BUFFER; } + return write_tlv_i32(v, result, result_len); + } else { return NYB_E_INVALID_HANDLE; } + } else { return NYB_E_PLUGIN_ERROR; } + } + METHOD_GET => { + if let Ok(map) = INSTANCES.lock() { + if let Some(inst) = map.get(&instance_id) { + if preflight(result, result_len, 12) { return NYB_E_SHORT_BUFFER; } + return write_tlv_i32(inst.count, result, result_len); + } else { return NYB_E_INVALID_HANDLE; } + } else { return NYB_E_PLUGIN_ERROR; } + } + _ => NYB_E_INVALID_METHOD, + } + } +} + +// ===== TLV helpers ===== +fn write_tlv_result(payloads: &[(u8, &[u8])], result: *mut u8, result_len: *mut usize) -> i32 { + if result_len.is_null() { return NYB_E_INVALID_ARGS; } + let mut buf: Vec = Vec::with_capacity(4 + payloads.iter().map(|(_,p)| 4 + p.len()).sum::()); + buf.extend_from_slice(&1u16.to_le_bytes()); // version + buf.extend_from_slice(&(payloads.len() as u16).to_le_bytes()); // argc + for (tag, payload) in payloads { + buf.push(*tag); + buf.push(0); + buf.extend_from_slice(&(payload.len() as u16).to_le_bytes()); + buf.extend_from_slice(payload); + } + unsafe { + let needed = buf.len(); + if result.is_null() || *result_len < needed { + *result_len = needed; + return NYB_E_SHORT_BUFFER; + } + std::ptr::copy_nonoverlapping(buf.as_ptr(), result, needed); + *result_len = needed; + } + NYB_SUCCESS +} + +fn write_tlv_i32(v: i32, result: *mut u8, result_len: *mut usize) -> i32 { + write_tlv_result(&[(2u8, &v.to_le_bytes())], result, result_len) +} + +fn preflight(result: *mut u8, result_len: *mut usize, needed: usize) -> bool { + unsafe { + if result_len.is_null() { return false; } + if result.is_null() || *result_len < needed { + *result_len = needed; + return true; + } + } + false +} + diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index 5f0ff4e4..37163ee5 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -234,17 +234,21 @@ impl NyashInterpreter { /// 新しいインタープリターを作成 pub fn new() -> Self { let shared = SharedState::new(); - - // ランタイムを構築し、ユーザー定義Boxファクトリを注入(グローバル登録を避ける) - use crate::box_factory::user_defined::UserDefinedBoxFactory; - let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); - let runtime = NyashRuntimeBuilder::new().with_factory(udf).build(); - // Step 5: SharedState分解の第一歩として、 - // box_declarationsの保管先をRuntimeに寄せる + // 先にランタイムを構築(UDFは後から同一SharedStateで注入) + let runtime = NyashRuntimeBuilder::new().build(); + + // Runtimeのbox_declarationsを共有状態に差し替え、同一の参照を保つ let mut shared = shared; // 可変化 shared.box_declarations = runtime.box_declarations.clone(); - + + // ユーザー定義Boxファクトリを、差し替え済みSharedStateで登録 + use crate::box_factory::user_defined::UserDefinedBoxFactory; + let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + if let Ok(mut reg) = runtime.box_registry.lock() { + reg.register(udf); + } + Self { shared, local_vars: HashMap::new(), @@ -262,16 +266,22 @@ impl NyashInterpreter { pub fn new_with_groups(groups: crate::box_factory::builtin::BuiltinGroups) -> Self { let shared = SharedState::new(); - use crate::box_factory::user_defined::UserDefinedBoxFactory; - let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + // 先にランタイム(組み込みグループのみ)を構築 let runtime = NyashRuntimeBuilder::new() .with_builtin_groups(groups) - .with_factory(udf) .build(); + // Runtimeのbox_declarationsを共有状態に差し替え let mut shared = shared; // 可変化 shared.box_declarations = runtime.box_declarations.clone(); + // 差し替え済みSharedStateでUDFを登録 + use crate::box_factory::user_defined::UserDefinedBoxFactory; + let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + if let Ok(mut reg) = runtime.box_registry.lock() { + reg.register(udf); + } + Self { shared, local_vars: HashMap::new(), @@ -287,15 +297,20 @@ impl NyashInterpreter { /// 共有状態から新しいインタープリターを作成(非同期実行用) pub fn with_shared(shared: SharedState) -> Self { - // 共有状態に紐づいたランタイムを構築 - use crate::box_factory::user_defined::UserDefinedBoxFactory; - let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); - let runtime = NyashRuntimeBuilder::new().with_factory(udf).build(); + // 共有状態に紐づいたランタイムを先に構築 + let runtime = NyashRuntimeBuilder::new().build(); - // Step 5: Runtimeのbox_declarationsに寄せ替え + // Runtimeのbox_declarationsに寄せ替え let mut shared = shared; // 可変化 shared.box_declarations = runtime.box_declarations.clone(); - + + // 差し替え済みSharedStateでUDFを登録 + use crate::box_factory::user_defined::UserDefinedBoxFactory; + let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + if let Ok(mut reg) = runtime.box_registry.lock() { + reg.register(udf); + } + Self { shared, local_vars: HashMap::new(), @@ -311,16 +326,21 @@ impl NyashInterpreter { /// 共有状態+グループ構成を指定して新しいインタープリターを作成(非同期実行用) pub fn with_shared_and_groups(shared: SharedState, groups: crate::box_factory::builtin::BuiltinGroups) -> Self { - use crate::box_factory::user_defined::UserDefinedBoxFactory; - let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + // 先にランタイム(組み込みグループのみ)を構築 let runtime = NyashRuntimeBuilder::new() .with_builtin_groups(groups) - .with_factory(udf) .build(); let mut shared = shared; // 可変化 shared.box_declarations = runtime.box_declarations.clone(); + // 差し替え済みSharedStateでUDFを登録 + use crate::box_factory::user_defined::UserDefinedBoxFactory; + let udf = Arc::new(UserDefinedBoxFactory::new(shared.clone())); + if let Ok(mut reg) = runtime.box_registry.lock() { + reg.register(udf); + } + Self { shared, local_vars: HashMap::new(), diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index f901e567..f089f016 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -749,6 +749,54 @@ impl NyashInterpreter { }); } + // 先にプラグイン親のコンストラクタ/メソッドを優先的に処理(v2プラグイン対応) + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + { + let loader_guard = crate::runtime::get_global_loader_v2(); + let loader = loader_guard.read().unwrap(); + // 親がプラグインで提供されているかを確認 + if loader.config.as_ref().and_then(|c| c.find_library_for_box(parent)).is_some() { + // コンストラクタ相当(birth もしくは 親名と同名)の場合は、 + // プラグインBoxを生成して __plugin_content に格納 + if method == "birth" || method == parent { + let mut arg_values: Vec> = Vec::new(); + for arg in arguments { + arg_values.push(self.execute_expression(arg)?); + } + match loader.create_box(parent, &arg_values) { + Ok(pbox) => { + use std::sync::Arc; + let _ = current_instance.set_field_legacy("__plugin_content", Arc::from(pbox)); + return Ok(Box::new(crate::box_trait::VoidBox::new())); + } + Err(e) => { + return Err(RuntimeError::InvalidOperation { + message: format!("Failed to construct plugin parent '{}': {:?}", parent, e), + }); + } + } + } else { + // 非コンストラクタ: 既存の __plugin_content を通じてメソッド呼び出し + if let Some(plugin_shared) = current_instance.get_field_legacy("__plugin_content") { + let plugin_ref = &*plugin_shared; + if let Some(plugin) = plugin_ref.as_any().downcast_ref::() { + let mut arg_values: Vec> = Vec::new(); + for arg in arguments { arg_values.push(self.execute_expression(arg)?); } + match loader.invoke_instance_method(&plugin.box_type, method, plugin.instance_id, &arg_values) { + Ok(Some(result_box)) => return Ok(result_box), + Ok(None) => return Ok(Box::new(crate::box_trait::VoidBox::new())), + Err(e) => { + return Err(RuntimeError::InvalidOperation { + message: format!("Plugin call {}.{} failed: {:?}", parent, method, e), + }); + } + } + } + } + } + } + } + // 🔥 Phase 8.8: pack透明化システム - ビルトインBox判定 use crate::box_trait::is_builtin_box; // GUI機能が有効な場合はEguiBoxも追加判定(mut不要の形に) diff --git a/tests/e2e_plugin_counterbox.rs b/tests/e2e_plugin_counterbox.rs new file mode 100644 index 00000000..a6a6b2f0 --- /dev/null +++ b/tests/e2e_plugin_counterbox.rs @@ -0,0 +1,75 @@ +#![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!("[e2e] init_global_loader_v2 failed: {:?}", e); + return false; + } + // Map all configured boxes to plugin providers for legacy registry + 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_name, lib_def) in &conf.libraries { + for b in &lib_def.boxes { map.insert(b.clone(), lib_name.clone()); } + } + let reg = get_global_registry(); + reg.apply_plugin_config(&PluginConfig { plugins: map }); + true + } else { false } +} + +#[test] +fn e2e_counter_basic_inc_get() { + if !try_init_plugins() { return; } + + let code = r#" +local c, v1, v2 +c = new CounterBox() +v1 = c.get() +c.inc() +v2 = c.get() +v2 +"#; + let ast = NyashParser::parse_from_string(code).expect("parse failed"); + let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new(); + + match interpreter.execute(ast) { + Ok(result) => { + // After one inc(), count should be 1 + assert_eq!(result.to_string_box().value, "1"); + } + Err(e) => panic!("Counter basic test failed: {:?}", e), + } +} + +#[test] +fn e2e_counter_assignment_clones_not_shares() { + if !try_init_plugins() { return; } + + let code = r#" +local c, x, v +c = new CounterBox() +x = c +x.inc() +v = c.get() +v +"#; + let ast = NyashParser::parse_from_string(code).expect("parse failed"); + let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new(); + + match interpreter.execute(ast) { + Ok(result) => { + // Current semantics: assignment clones (not shares), so c remains 0 + assert_eq!(result.to_string_box().value, "0"); + } + Err(e) => panic!("Counter assignment test failed: {:?}", e), + } +} + diff --git a/tests/e2e_plugin_filebox.rs b/tests/e2e_plugin_filebox.rs index ac13b94c..500b3a98 100644 --- a/tests/e2e_plugin_filebox.rs +++ b/tests/e2e_plugin_filebox.rs @@ -51,10 +51,9 @@ f.close() match interpreter.execute(ast) { Ok(result) => { - // close() returns void + // close() returns void (BID-1 tag=9) let result_str = result.to_string_box().value; - // FileBoxの戻り値は現在 "ok" を返すので、それで確認 - assert_eq!(result_str, "ok", "Expected 'ok' result from close()"); + assert_eq!(result_str, "void", "Expected 'void' result from close()"); println!("✅ E2E Plugin FileBox Interpreter test passed!"); } Err(e) => { @@ -122,3 +121,34 @@ f.close() assert_eq!(result.to_string_box().value, "void"); } +#[test] +fn e2e_interpreter_plugin_filebox_copy_from_handle() { + if !try_init_plugins() { return; } + + // Prepare two files and copy contents via plugin Handle argument + let p1 = "./test_out_src.txt"; + let p2 = "./test_out_dst.txt"; + + // Nyash program: open two FileBox, write to src, copy to dst via copyFrom, then read dst + let code = format!(r#" +local a, b, data +a = new FileBox() +b = new FileBox() +a.open("{}", "w") +b.open("{}", "rw") +a.write("HELLO") +b.copyFrom(a) +data = b.read() +data +"#, p1, p2); + + let ast = NyashParser::parse_from_string(&code).expect("parse failed"); + let mut interpreter = nyash_rust::interpreter::NyashInterpreter::new(); + + match interpreter.execute(ast) { + Ok(result) => { + assert_eq!(result.to_string_box().value, "HELLO"); + } + Err(e) => panic!("Failed to execute copyFrom test: {:?}", e), + } +} diff --git a/tests/e2e_reserved_name_guard.rs b/tests/e2e_reserved_name_guard.rs new file mode 100644 index 00000000..170e7181 --- /dev/null +++ b/tests/e2e_reserved_name_guard.rs @@ -0,0 +1,65 @@ +//! E2E: Reserved-name guard for unified registry +use std::sync::Arc; + +use nyash_rust::box_factory::BoxFactory; +use nyash_rust::box_factory::builtin::BuiltinGroups; +use nyash_rust::interpreter::NyashInterpreter; +use nyash_rust::interpreter::RuntimeError; +use nyash_rust::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox}; + +// Dummy factory that tries to claim reserved core types +struct BadFactory; +impl BadFactory { fn new() -> Self { Self } } + +#[derive(Debug, Clone)] +struct FakeStringBox { base: BoxBase, inner: String } + +impl BoxCore for FakeStringBox { + 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, "FakeString(\"{}\")", self.inner) } + fn as_any(&self) -> &dyn std::any::Any { self } + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } +} + +impl NyashBox for FakeStringBox { + fn to_string_box(&self) -> StringBox { StringBox::new(format!("FAKE:{}", self.inner)) } + fn equals(&self, other: &dyn NyashBox) -> BoolBox { + if let Some(s) = other.as_any().downcast_ref::() { BoolBox::new(self.inner == s.inner) } else { BoolBox::new(false) } + } + fn type_name(&self) -> &'static str { "StringBox" } + fn clone_box(&self) -> Box { Box::new(self.clone()) } + fn share_box(&self) -> Box { Box::new(self.clone()) } +} + +impl BoxFactory for BadFactory { + fn create_box(&self, name: &str, args: &[Box]) -> Result, RuntimeError> { + match name { + // Attempt to hijack StringBox + "StringBox" => { + let s = args.get(0).map(|a| a.to_string_box().value).unwrap_or_default(); + Ok(Box::new(FakeStringBox { base: BoxBase::new(), inner: s })) + } + _ => Err(RuntimeError::InvalidOperation { message: format!("Unknown Box type: {}", name) }) + } + } + fn box_types(&self) -> Vec<&str> { vec!["StringBox"] } +} + +#[test] +fn e2e_reserved_name_guard_rejects_non_builtin_registration() { + // Interpreter with all builtins + let mut i = NyashInterpreter::new_with_groups(BuiltinGroups::native_full()); + // Register bad factory; registry should reject claiming reserved types + i.register_box_factory(Arc::new(BadFactory::new())); + + // Creating a StringBox must still use builtin behavior (no FAKE: prefix) + let code = r#" + s = new StringBox("ok") + s + "#; + 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, "ok"); +} +