diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 3b5b14bd..10274255 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -397,3 +397,16 @@ bash tools/build_aot.sh examples/aot_min_string_len.nyash -o app - 状態確認: `git status` / `git log --oneline -3` / `cargo check` - スモーク: `bash tools/smoke_phase_10_10.sh` - 次の一手: core_hostcall → core_ops の順に分割、毎回ビルド/スモークで確認 + +--- + +### 新規フェーズ(提案): Phase 10.11 Builtins → Plugins 移行 +- 目的: 内蔵Box経路を段階的に廃止し、プラグイン/ユーザーBoxに一本化する(不具合の温床を解消) +- 現在の足場(済): + - ConsoleBox コンストラクタをレジストリ委譲(プラグイン優先)に変更 + - `NYASH_DISABLE_BUILTINS=1` でビルトインFactory登録を抑止可能 + - 設計ドキュメント: docs/development/roadmap/phases/phase-10.11-builtins-to-plugins.md +- 次ステップ: + - 非基本コンストラクタの委譲徹底(Math/Random/Sound/Debugなど) + - 主要ビルトインの plugin 化(nyash_box.toml 整備) + - CIに `NYASH_USE_PLUGIN_BUILTINS=1` / `NYASH_PLUGIN_OVERRIDE_TYPES` のスモークを追加 diff --git a/docs/development/roadmap/phases/phase-10.11-builtins-to-plugins.md b/docs/development/roadmap/phases/phase-10.11-builtins-to-plugins.md new file mode 100644 index 00000000..b152e26d --- /dev/null +++ b/docs/development/roadmap/phases/phase-10.11-builtins-to-plugins.md @@ -0,0 +1,54 @@ +# Phase 10.11: Builtins → Plugins Migration + +## Goals +- Remove builtin Box implementations from execution paths (Interpreter/VM/JIT) to avoid divergence and double sources of truth. +- Provide all functionality via plugins (BID-FFI v1) and/or user-defined boxes. +- Keep backward compatibility guarded behind env flags until migration completes. + +## Rationale +- Conflicts like ConsoleBox builtin vs plugin cause unexpected behavior. +- Native build (AOT/EXE) path benefits from uniform plugin boundary. +- One registry, one implementation per Box: simpler, safer. + +## Plan (Incremental) +1) Disable Switch (Now) +- Add `NYASH_DISABLE_BUILTINS=1` to skip registering builtin box factory. +- Keep off by default; use in CI lanes and targeted tests. + +2) Constructor Delegation (Now → Next) +- Ensure all constructors go through the unified registry, not direct builtin instantiation. +- Done: ConsoleBox; Next: remaining non-basic constructors. + +3) Override Policy (Ongoing) +- Use `NYASH_USE_PLUGIN_BUILTINS=1` + `NYASH_PLUGIN_OVERRIDE_TYPES` to prefer plugins for selected types. +- Grow the allowlist as plugins become available. + +4) Plugin Coverage (Milestones) +- ConsoleBox (stdout) — done +- Array/Map/String/Integer — in place +- File/Net/Python — in place +- Math/Time/etc. — add `nyash_box.toml` and minimal plugins + +5) Remove Builtins (Final) +- Remove builtin factory or move into separate optional crate for legacy runs. +- Update docs, examples, and CI to plugin-only. + +## Acceptance Criteria +- `NYASH_DISABLE_BUILTINS=1` + plugin set → examples run green (VM path). +- No direct builtins in interpreter constructors (registry only). +- JIT/AOT compile from MIR uses only plugin invoke shims for Box methods. + +## How to Test +```bash +# Strict plugin preference + disable builtins +export NYASH_USE_PLUGIN_BUILTINS=1 +export NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox,ConsoleBox,StringBox,IntegerBox" +export NYASH_DISABLE_BUILTINS=1 + +cargo build --release --features cranelift-jit +./target/release/nyash --backend vm examples/console_demo.nyash +``` + +## Notes +- Temporary breakages expected when some builtin-only boxes remain. Use the override allowlist tactically. +- Keep `[libraries]` and `[plugins]` configured to ensure provider discovery. diff --git a/docs/reference/plugin-system/README.md b/docs/reference/plugin-system/README.md index 3ad5188d..5aff1c6d 100644 --- a/docs/reference/plugin-system/README.md +++ b/docs/reference/plugin-system/README.md @@ -125,6 +125,25 @@ cargo build --release ./target/release/plugin-tester check path/to/your/plugin.so ``` +### 5. **nyash_box.toml テンプレ & スモーク** 🆕 +- テンプレート: `docs/reference/plugin-system/nyash_box.toml.template` +- スモーク実行(VM・厳格チェックON): +```bash +bash tools/smoke_plugins.sh +``` + - 実行内容: Python デモと Integer デモを `NYASH_PLUGIN_STRICT=1` で起動し、nyash_box.toml 経路のロードと実行を確認 + - 事前条件: `cargo build --release --features cranelift-jit` 済み、各プラグインも release ビルド済み + +### 6. **プラグイン優先(ビルトイン上書き)設定** 🆕 +- 既定では、ビルトインの実装が優先されます(安全第一)。 +- プラグインで置き換えたい型(ConsoleBox など)がある場合は環境変数で上書き可能: +```bash +export NYASH_USE_PLUGIN_BUILTINS=1 +export NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox,ConsoleBox" +``` + - 上記により、`new ConsoleBox()` などの生成がプラグイン経路に切替わります。 + - 後方互換のため `[libraries]` にも対象プラグインを登録しておくと、解決の一貫性が高まります。 + ## 🔧 For Nyash Core Developers ### Implementation Files diff --git a/docs/reference/plugin-system/nyash_box.toml.template b/docs/reference/plugin-system/nyash_box.toml.template new file mode 100644 index 00000000..5fe60212 --- /dev/null +++ b/docs/reference/plugin-system/nyash_box.toml.template @@ -0,0 +1,31 @@ +[box] +name = "" +version = "0.1.0" +description = "" +author = "" + +[provides] +boxes = ["", ""] + +[] +type_id = + +[.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } # optional + +[.methods.] +id = +args = [ { name = "", type = "" } ] # optional +returns = { type = "", error = "string" } # optional +returns_result = # optional: Ok/Err(ResultBox)に正規化 + +[implementation] +ffi_version = 1 +thread_safe = + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/.dll" +linux = "target/release/lib.so" +macos = "target/release/lib.dylib" + diff --git a/examples/console_demo.nyash b/examples/console_demo.nyash new file mode 100644 index 00000000..3965a54a --- /dev/null +++ b/examples/console_demo.nyash @@ -0,0 +1,13 @@ +// ConsoleBox plugin demo + +static box Main { + main() { + local c + c = new ConsoleBox() + c.println("Hello from ConsoleBox!") + c.log("no newline...") + c.println(" done") + return 0 + } +} + diff --git a/examples/console_demo_simple.nyash b/examples/console_demo_simple.nyash new file mode 100644 index 00000000..3db5f9c8 --- /dev/null +++ b/examples/console_demo_simple.nyash @@ -0,0 +1,13 @@ +// Simple ConsoleBox test +local c +c = new ConsoleBox() + +// First, let's see if the box was created +print("ConsoleBox created: " + c) + +// Now try to call methods +c.println("Hello from ConsoleBox println!") +c.log("Hello from log...") + +// Use traditional print too +print("Traditional print works!") \ No newline at end of file diff --git a/examples/demo_boxes_native.nyash b/examples/demo_boxes_native.nyash new file mode 100644 index 00000000..ef73ea72 --- /dev/null +++ b/examples/demo_boxes_native.nyash @@ -0,0 +1,28 @@ +// Demo for native build +print("📦 1. Basic Box Creation:") + +local str +str = new StringBox("Hello, Nyash!") +print(" StringBox: " + str) + +local num +num = new IntegerBox(42) +print(" IntegerBox: " + num) + +local bool +bool = new BoolBox(true) +print(" BoolBox: " + bool) + +print("") +print("🔄 2. Box Operations:") + +local result +result = new IntegerBox(10) + new IntegerBox(32) +print(" 10 + 32 = " + result) + +local hello +hello = new StringBox("Hello, ") +local world +world = new StringBox("World!") +result = hello + world +print(" \"Hello, \" + \"World!\" = " + result) \ No newline at end of file diff --git a/examples/hello_native.nyash b/examples/hello_native.nyash new file mode 100644 index 00000000..65ab0a6a --- /dev/null +++ b/examples/hello_native.nyash @@ -0,0 +1,9 @@ +// Hello World for Native Build Demo +print("🎉 Nyash Native Build Success!") +print("Platform: " + "Linux/Windows") +print("Compiled with Cranelift JIT") + +// 簡単な計算も +local result +result = 42 * 2 +print("The answer is: " + result) \ No newline at end of file diff --git a/examples/hello_simple_native.nyash b/examples/hello_simple_native.nyash new file mode 100644 index 00000000..b15ca678 --- /dev/null +++ b/examples/hello_simple_native.nyash @@ -0,0 +1,2 @@ +// Ultra simple for native build +print("Hello from Native Nyash!") \ No newline at end of file diff --git a/examples/integer_plugin_demo_fixed.nyash b/examples/integer_plugin_demo_fixed.nyash new file mode 100644 index 00000000..27237ceb --- /dev/null +++ b/examples/integer_plugin_demo_fixed.nyash @@ -0,0 +1,15 @@ +// IntegerBox plugin demo with ConsoleBox +static box Main { + main() { + local c + c = new ConsoleBox() + + local i + i = new IntegerBox() + i.set(42) + c.println("IntegerBox set to 42") + c.println("get() = " + i.get()) + + return i.get() + } +} \ No newline at end of file diff --git a/examples/math_time_demo.nyash b/examples/math_time_demo.nyash new file mode 100644 index 00000000..ddcc93fa --- /dev/null +++ b/examples/math_time_demo.nyash @@ -0,0 +1,17 @@ +// Math/Time plugin demo + +static box Main { + main() { + local m, t, r + m = new MathBox() + r = m.sqrt(16) + t = new TimeBox() + // print via ConsoleBox + local c + c = new ConsoleBox() + c.println("sqrt(16) = " + r.toString()) + c.println("now = " + t.now().toString()) + return 0 + } +} + diff --git a/nyash.toml b/nyash.toml index c990bf3c..476f8fa4 100644 --- a/nyash.toml +++ b/nyash.toml @@ -128,6 +128,40 @@ search_paths = [ "/usr/local/lib/nyash/plugins", "~/.nyash/plugins" ] + +# 中央タイプIDレジストリ(新): 各プラグインの nyash_box.toml と一致させる +[box_types] +FileBox = 6 +ConsoleBox = 5 +StringBox = 10 +ArrayBox = 11 +MapBox = 12 +IntegerBox = 12 +CounterBox = 7 +HttpServerBox = 20 +HttpRequestBox = 21 +HttpResponseBox = 22 +HttpClientBox = 23 +SocketServerBox = 30 +SocketConnBox = 31 +SocketClientBox = 32 +MathBox = 50 +TimeBox = 51 +PyRuntimeBox= 40 +PyObjectBox = 41 + +# 新スタイルのプラグインルート(併用可・[libraries]は後方互換) +[plugins] +"libnyash_filebox_plugin" = "./plugins/nyash-filebox-plugin" +"libnyash_console_plugin" = "./plugins/nyash-console-plugin" +"libnyash_string_plugin" = "./plugins/nyash-string-plugin" +"libnyash_map_plugin" = "./plugins/nyash-map-plugin" +"libnyash_array_plugin" = "./plugins/nyash-array-plugin" +"libnyash_python_plugin" = "./plugins/nyash-python-plugin" +"libnyash_integer_plugin" = "./plugins/nyash-integer-plugin" +"libnyash_counter_plugin" = "./plugins/nyash-counter-plugin" +"libnyash_net_plugin" = "./plugins/nyash-net-plugin" +"libnyash_math_plugin" = "./plugins/nyash-math-plugin" [libraries."libnyash_array_plugin"] boxes = ["ArrayBox"] path = "./plugins/nyash-array-plugin/target/release/libnyash_array_plugin" @@ -217,3 +251,37 @@ fini = { method_id = 4294967295 } getattrR= { method_id = 11, args = ["name"], returns_result = true } callR = { method_id = 12, args = ["args"], returns_result = true } callKwR = { method_id = 15, returns_result = true } +[libraries."libnyash_console_plugin"] +boxes = ["ConsoleBox"] +path = "./plugins/nyash-console-plugin/target/release/libnyash_console_plugin" + +[libraries."libnyash_console_plugin".ConsoleBox] +type_id = 5 + +[libraries."libnyash_console_plugin".ConsoleBox.methods] +birth = { method_id = 0 } +log = { method_id = 1, args = ["text"] } +println = { method_id = 2, args = ["text"] } +fini = { method_id = 4294967295 } +[libraries."libnyash_math_plugin"] +boxes = ["MathBox", "TimeBox"] +path = "./plugins/nyash-math-plugin/target/release/libnyash_math_plugin" + +[libraries."libnyash_math_plugin".MathBox] +type_id = 50 + +[libraries."libnyash_math_plugin".MathBox.methods] +birth = { method_id = 0 } +sqrt = { method_id = 1, args = ["x"] } +sin = { method_id = 2, args = ["x"] } +cos = { method_id = 3, args = ["x"] } +round = { method_id = 4, args = ["x"] } +fini = { method_id = 4294967295 } + +[libraries."libnyash_math_plugin".TimeBox] +type_id = 51 + +[libraries."libnyash_math_plugin".TimeBox.methods] +birth = { method_id = 0 } +now = { method_id = 1 } +fini = { method_id = 4294967295 } diff --git a/plugins/nyash-array-plugin/nyash_box.toml b/plugins/nyash-array-plugin/nyash_box.toml new file mode 100644 index 00000000..3b520ed9 --- /dev/null +++ b/plugins/nyash-array-plugin/nyash_box.toml @@ -0,0 +1,40 @@ +[box] +name = "ArrayBox" +version = "1.0.0" +description = "Array operations Box" +author = "Nyash Team" + +[provides] +boxes = ["ArrayBox"] + +[ArrayBox] +type_id = 11 + +[ArrayBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[ArrayBox.methods.len] +id = 1 +args = [] +returns = { type = "i64" } + +[ArrayBox.methods.get] +id = 2 +args = [ { name = "index", type = "i64" } ] +returns = { type = "box" } + +[ArrayBox.methods.push] +id = 3 +args = [ { name = "value", type = "box" } ] +returns = { type = "i64" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_array_plugin.dll" +linux = "target/release/libnyash_array_plugin.so" +macos = "target/release/libnyash_array_plugin.dylib" + diff --git a/plugins/nyash-console-plugin/Cargo.toml b/plugins/nyash-console-plugin/Cargo.toml new file mode 100644 index 00000000..af245903 --- /dev/null +++ b/plugins/nyash-console-plugin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nyash-console-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +once_cell = "1.20" + +[profile.release] +lto = true +strip = true +opt-level = "z" + diff --git a/plugins/nyash-console-plugin/nyash_box.toml b/plugins/nyash-console-plugin/nyash_box.toml new file mode 100644 index 00000000..fce1d914 --- /dev/null +++ b/plugins/nyash-console-plugin/nyash_box.toml @@ -0,0 +1,35 @@ +[box] +name = "ConsoleBox" +version = "0.1.0" +description = "Standard output (stdout) printing" +author = "Nyash Team" + +[provides] +boxes = ["ConsoleBox"] + +[ConsoleBox] +type_id = 5 + +[ConsoleBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[ConsoleBox.methods.log] +id = 1 +args = [ { name = "text", type = "string" } ] +returns = { type = "void" } + +[ConsoleBox.methods.println] +id = 2 +args = [ { name = "text", type = "string" } ] +returns = { type = "void" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_console_plugin.dll" +linux = "target/release/libnyash_console_plugin.so" +macos = "target/release/libnyash_console_plugin.dylib" + diff --git a/plugins/nyash-console-plugin/src/lib.rs b/plugins/nyash-console-plugin/src/lib.rs new file mode 100644 index 00000000..19f2f441 --- /dev/null +++ b/plugins/nyash-console-plugin/src/lib.rs @@ -0,0 +1,133 @@ +//! Nyash ConsoleBox Plugin - BID-FFI v1 +//! Provides simple stdout printing via ConsoleBox + +use std::collections::HashMap; +use std::os::raw::c_char; +use std::sync::{Mutex, atomic::{AtomicU32, Ordering}}; + +// ===== Error Codes (BID-1) ===== +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; + +// ===== Method IDs ===== +const METHOD_BIRTH: u32 = 0; +const METHOD_LOG: u32 = 1; // log(text) +const METHOD_PRINTLN: u32 = 2; // println(text) +const METHOD_FINI: u32 = u32::MAX; + +// ===== Type ID ===== +const TYPE_ID_CONSOLE_BOX: u32 = 5; // keep in sync with nyash.toml [box_types] + +// ===== Instance management ===== +struct ConsoleInstance { /* no state for now */ } + +use once_cell::sync::Lazy; +static INSTANCES: Lazy>> = Lazy::new(|| { + Mutex::new(HashMap::new()) +}); +static INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(1); + +// ===== TLV helpers (minimal) ===== +// TLV layout: [u16 ver=1][u16 argc][entries...] +// Entry: [u16 tag][u16 size][payload...] +fn parse_first_string(args: &[u8]) -> Result { + if args.len() < 4 { return Err(()); } + let argc = u16::from_le_bytes([args[2], args[3]]) as usize; + if argc == 0 { return Err(()); } + let mut p = 4usize; + // first entry + if args.len() < p + 4 { return Err(()); } + let tag = u16::from_le_bytes([args[p], args[p+1]]); p += 2; + let sz = u16::from_le_bytes([args[p], args[p+1]]) as usize; p += 2; + if tag != 6 && tag != 7 { // String or Bytes + return Err(()); + } + if args.len() < p + sz { return Err(()); } + let s = String::from_utf8_lossy(&args[p..p+sz]).to_string(); + Ok(s) +} + +// Write TLV birth result: Handle(tag=8,size=8) with (type_id, instance_id) +unsafe fn write_tlv_birth(type_id: u32, instance_id: u32, out: *mut u8, out_len: *mut usize) -> i32 { + let need = 4 + 4 + 8; // header + entry + payload + if *out_len < need { *out_len = need; return NYB_E_SHORT_BUFFER; } + let mut buf = Vec::with_capacity(need); + // header + buf.extend_from_slice(&1u16.to_le_bytes()); + buf.extend_from_slice(&1u16.to_le_bytes()); + // entry: Handle + buf.extend_from_slice(&8u16.to_le_bytes()); + buf.extend_from_slice(&8u16.to_le_bytes()); + buf.extend_from_slice(&type_id.to_le_bytes()); + buf.extend_from_slice(&instance_id.to_le_bytes()); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, need); + *out_len = need; + NYB_SUCCESS +} + +unsafe fn write_tlv_void(out: *mut u8, out_len: *mut usize) -> i32 { + let need = 4 + 4; // header + entry + if *out_len < need { *out_len = need; return NYB_E_SHORT_BUFFER; } + let mut buf = Vec::with_capacity(need); + buf.extend_from_slice(&1u16.to_le_bytes()); + buf.extend_from_slice(&1u16.to_le_bytes()); + buf.extend_from_slice(&9u16.to_le_bytes()); // Void + buf.extend_from_slice(&0u16.to_le_bytes()); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, need); + *out_len = need; + NYB_SUCCESS +} + +// ===== Entry points ===== +#[no_mangle] +pub extern "C" fn nyash_plugin_abi() -> u32 { 1 } + +#[no_mangle] +pub extern "C" fn nyash_plugin_init() -> i32 { + eprintln!("[ConsoleBox] Plugin initialized"); + 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_CONSOLE_BOX { return NYB_E_INVALID_TYPE; } + unsafe { + match method_id { + METHOD_BIRTH => { + let id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + if let Ok(mut m) = INSTANCES.lock() { + m.insert(id, ConsoleInstance{}); + } else { return NYB_E_PLUGIN_ERROR; } + return write_tlv_birth(TYPE_ID_CONSOLE_BOX, id, result, result_len); + } + METHOD_FINI => { + if let Ok(mut m) = INSTANCES.lock() { m.remove(&instance_id); } + return NYB_SUCCESS; + } + METHOD_LOG | METHOD_PRINTLN => { + let slice = std::slice::from_raw_parts(args, args_len); + match parse_first_string(slice) { + Ok(s) => { + if method_id == METHOD_LOG { print!("{}", s); } else { println!("{}", s); } + return write_tlv_void(result, result_len); + } + Err(_) => return NYB_E_INVALID_ARGS, + } + } + _ => NYB_E_INVALID_METHOD, + } + } +} + diff --git a/plugins/nyash-counter-plugin/nyash_box.toml b/plugins/nyash-counter-plugin/nyash_box.toml new file mode 100644 index 00000000..a79b700c --- /dev/null +++ b/plugins/nyash-counter-plugin/nyash_box.toml @@ -0,0 +1,35 @@ +[box] +name = "CounterBox" +version = "1.0.0" +description = "Process-wide counter (singleton)" +author = "Nyash Team" + +[provides] +boxes = ["CounterBox"] + +[CounterBox] +type_id = 7 + +[CounterBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[CounterBox.methods.inc] +id = 1 +args = [] +returns = { type = "i64" } + +[CounterBox.methods.get] +id = 2 +args = [] +returns = { type = "i64" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_counter_plugin.dll" +linux = "target/release/libnyash_counter_plugin.so" +macos = "target/release/libnyash_counter_plugin.dylib" + diff --git a/plugins/nyash-filebox-plugin/nyash_box.toml b/plugins/nyash-filebox-plugin/nyash_box.toml new file mode 100644 index 00000000..a8df2187 --- /dev/null +++ b/plugins/nyash-filebox-plugin/nyash_box.toml @@ -0,0 +1,35 @@ +[box] +name = "FileBox" +version = "1.0.0" +description = "File I/O operations Box" +author = "Nyash Team" + +[provides] +boxes = ["FileBox"] + +[FileBox] +type_id = 6 + +[FileBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[FileBox.methods.open] +id = 1 +args = [ { name = "path", type = "string" }, { name = "mode", type = "string", default = "r" } ] +returns = { type = "void", error = "string" } + +[FileBox.methods.read] +id = 2 +args = [] +returns = { type = "string" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_filebox_plugin.dll" +linux = "target/release/libnyash_filebox_plugin.so" +macos = "target/release/libnyash_filebox_plugin.dylib" + diff --git a/plugins/nyash-integer-plugin/nyash_box.toml b/plugins/nyash-integer-plugin/nyash_box.toml new file mode 100644 index 00000000..a931f12f --- /dev/null +++ b/plugins/nyash-integer-plugin/nyash_box.toml @@ -0,0 +1,35 @@ +[box] +name = "IntegerBox" +version = "1.0.0" +description = "Basic integer box (get/set)" +author = "Nyash Team" + +[provides] +boxes = ["IntegerBox"] + +[IntegerBox] +type_id = 12 + +[IntegerBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[IntegerBox.methods.get] +id = 1 +args = [] +returns = { type = "i64" } + +[IntegerBox.methods.set] +id = 2 +args = [ { name = "value", type = "i64" } ] +returns = { type = "void" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_integer_plugin.dll" +linux = "target/release/libnyash_integer_plugin.so" +macos = "target/release/libnyash_integer_plugin.dylib" + diff --git a/plugins/nyash-map-plugin/nyash_box.toml b/plugins/nyash-map-plugin/nyash_box.toml new file mode 100644 index 00000000..2c90c126 --- /dev/null +++ b/plugins/nyash-map-plugin/nyash_box.toml @@ -0,0 +1,35 @@ +[box] +name = "MapBox" +version = "1.0.0" +description = "Key-value map Box" +author = "Nyash Team" + +[provides] +boxes = ["MapBox"] + +[MapBox] +type_id = 12 + +[MapBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[MapBox.methods.size] +id = 1 +args = [] +returns = { type = "i64" } + +[MapBox.methods.get] +id = 2 +args = [ { name = "key", type = "i64" } ] +returns = { type = "box" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_map_plugin.dll" +linux = "target/release/libnyash_map_plugin.so" +macos = "target/release/libnyash_map_plugin.dylib" + diff --git a/plugins/nyash-math-plugin/Cargo.toml b/plugins/nyash-math-plugin/Cargo.toml new file mode 100644 index 00000000..3866d397 --- /dev/null +++ b/plugins/nyash-math-plugin/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nyash-math-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +once_cell = "1.20" + +[profile.release] +lto = true +strip = true +opt-level = "z" + diff --git a/plugins/nyash-math-plugin/nyash_box.toml b/plugins/nyash-math-plugin/nyash_box.toml new file mode 100644 index 00000000..5d5dc475 --- /dev/null +++ b/plugins/nyash-math-plugin/nyash_box.toml @@ -0,0 +1,56 @@ +[box] +name = "Nyash Math/Time Plugin" +version = "0.1.0" +description = "Minimal MathBox/TimeBox" +author = "Nyash Team" + +[provides] +boxes = ["MathBox", "TimeBox"] + +[MathBox] +type_id = 50 + +[MathBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[MathBox.methods.sqrt] +id = 1 +args = [ { name = "x", type = "i64" } ] +returns = { type = "f64" } + +[MathBox.methods.sin] +id = 2 +args = [ { name = "x", type = "i64" } ] +returns = { type = "f64" } + +[MathBox.methods.cos] +id = 3 +args = [ { name = "x", type = "i64" } ] +returns = { type = "f64" } + +[MathBox.methods.round] +id = 4 +args = [ { name = "x", type = "i64" } ] +returns = { type = "f64" } + +[TimeBox] +type_id = 51 + +[TimeBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[TimeBox.methods.now] +id = 1 +args = [] +returns = { type = "i64" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_math_plugin.dll" +linux = "target/release/libnyash_math_plugin.so" +macos = "target/release/libnyash_math_plugin.dylib" diff --git a/plugins/nyash-math-plugin/src/lib.rs b/plugins/nyash-math-plugin/src/lib.rs new file mode 100644 index 00000000..2038996a --- /dev/null +++ b/plugins/nyash-math-plugin/src/lib.rs @@ -0,0 +1,155 @@ +//! Nyash Math/Time Plugin - BID-FFI v1 (minimal) +//! MathBox: sqrt(i64) -> i64 +//! TimeBox: now() -> i64 (unix seconds) + +use std::collections::HashMap; +use std::sync::{Mutex, atomic::{AtomicU32, Ordering}}; + +// Error codes +const OK: i32 = 0; +const E_SHORT: i32 = -1; +const E_TYPE: i32 = -2; +const E_METHOD: i32 = -3; +const E_ARGS: i32 = -4; +const E_FAIL: i32 = -5; + +// Type IDs (align with nyash.toml [box_types]) +const TID_MATH: u32 = 50; +const TID_TIME: u32 = 51; + +// Methods +const M_BIRTH: u32 = 0; +const M_FINI: u32 = u32::MAX; +// MathBox +const M_SQRT: u32 = 1; +const M_SIN: u32 = 2; +const M_COS: u32 = 3; +const M_ROUND: u32 = 4; +// TimeBox +const T_NOW: u32 = 1; + +use once_cell::sync::Lazy; +#[derive(Default)] +struct Empty; +static MATH_INST: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static TIME_INST: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static ID: AtomicU32 = AtomicU32::new(1); + +// TLV helpers +mod tlv { + pub fn header(argc: u16) -> Vec { let mut b=Vec::with_capacity(4); b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&argc.to_le_bytes()); b } + pub fn encode_handle(buf: &mut Vec, t: u32, i: u32) { buf.push(8); buf.push(0); buf.push(8); buf.push(0); buf.extend_from_slice(&t.to_le_bytes()); buf.extend_from_slice(&i.to_le_bytes()); } + pub fn encode_i64(buf: &mut Vec, v: i64) { buf.push(3); buf.push(0); buf.push(8); buf.push(0); buf.extend_from_slice(&v.to_le_bytes()); } + pub fn encode_void(buf: &mut Vec) { buf.push(9); buf.push(0); buf.push(0); buf.push(0); } + pub fn decode_first(args:&[u8]) -> Option<(u16,u16,usize)> { if args.len()<8 {return None;} let argc=u16::from_le_bytes([args[2],args[3]]); if argc==0{return None;} let tag=u16::from_le_bytes([args[4],args[5]]); let sz=u16::from_le_bytes([args[6],args[7]]); Some((tag,sz,8)) } +} + +#[no_mangle] +pub extern "C" fn nyash_plugin_abi() -> u32 { 1 } + +#[no_mangle] +pub extern "C" fn nyash_plugin_init() -> i32 { OK } + +#[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 { + unsafe { + match (type_id, method_id) { + (TID_MATH, M_BIRTH) => birth(TID_MATH, &MATH_INST, result, result_len), + (TID_TIME, M_BIRTH) => birth(TID_TIME, &TIME_INST, result, result_len), + (TID_MATH, M_FINI) => fini(&MATH_INST, instance_id), + (TID_TIME, M_FINI) => fini(&TIME_INST, instance_id), + (TID_MATH, M_SQRT) => sqrt_call(args, args_len, result, result_len), + (TID_MATH, M_SIN) => trig_call(args, args_len, result, result_len, true), + (TID_MATH, M_COS) => trig_call(args, args_len, result, result_len, false), + (TID_MATH, M_ROUND) => round_call(args, args_len, result, result_len), + (TID_TIME, T_NOW) => now_call(result, result_len), + (TID_MATH, _) | (TID_TIME, _) => E_METHOD, + _ => E_TYPE, + } + } +} + +unsafe fn birth(tid: u32, map: &Lazy>>, out: *mut u8, out_len: *mut usize) -> i32 where T: Default { + let need = 4+4+8; if *out_len < need { *out_len = need; return E_SHORT; } + let id = ID.fetch_add(1, Ordering::Relaxed); + if let Ok(mut m) = map.lock() { m.insert(id, T::default()); } else { return E_FAIL; } + let mut buf = tlv::header(1); tlv::encode_handle(&mut buf, tid, id); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, buf.len()); *out_len = buf.len(); OK +} + +unsafe fn fini(map: &Lazy>>, instance_id: u32) -> i32 { + if let Ok(mut m) = map.lock() { m.remove(&instance_id); OK } else { E_FAIL } +} + +unsafe fn sqrt_call(args: *const u8, args_len: usize, out: *mut u8, out_len: *mut usize) -> i32 { + if args_len < 8 { return E_ARGS; } + let a = std::slice::from_raw_parts(args, args_len); + if let Some((tag, sz, p)) = tlv::decode_first(a) { + if tag == 3 && sz == 8 && a.len() >= p+8 { + let mut b=[0u8;8]; b.copy_from_slice(&a[p..p+8]); + let x = i64::from_le_bytes(b) as f64; + let r = x.sqrt(); + let need = 4+4+8; if *out_len < need { *out_len = need; return E_SHORT; } + let mut buf = tlv::header(1); + // encode f64 (tag=5) + buf.push(5); buf.push(0); buf.push(8); buf.push(0); buf.extend_from_slice(&r.to_le_bytes()); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, buf.len()); *out_len = buf.len(); + return OK; + } + } + E_ARGS +} + +unsafe fn now_call(out: *mut u8, out_len: *mut usize) -> i32 { + let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0); + let need = 4+4+8; if *out_len < need { *out_len = need; return E_SHORT; } + let mut buf = tlv::header(1); tlv::encode_i64(&mut buf, ts); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, buf.len()); *out_len = buf.len(); + OK +} + +unsafe fn trig_call(args: *const u8, args_len: usize, out: *mut u8, out_len: *mut usize, is_sin: bool) -> i32 { + if args_len < 8 { return E_ARGS; } + let a = std::slice::from_raw_parts(args, args_len); + if let Some((tag, sz, p)) = tlv::decode_first(a) { + if tag == 3 && sz == 8 && a.len() >= p+8 { + let mut b=[0u8;8]; b.copy_from_slice(&a[p..p+8]); + let x = i64::from_le_bytes(b) as f64; + let r = if is_sin { x.sin() } else { x.cos() }; + let need = 4+4+8; if *out_len < need { *out_len = need; return E_SHORT; } + let mut buf = tlv::header(1); + // encode f64 (tag=5) + buf.push(5); buf.push(0); buf.push(8); buf.push(0); buf.extend_from_slice(&r.to_le_bytes()); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, buf.len()); *out_len = buf.len(); + return OK; + } + } + E_ARGS +} + +unsafe fn round_call(args: *const u8, args_len: usize, out: *mut u8, out_len: *mut usize) -> i32 { + if args_len < 8 { return E_ARGS; } + let a = std::slice::from_raw_parts(args, args_len); + if let Some((tag, sz, p)) = tlv::decode_first(a) { + if tag == 3 && sz == 8 && a.len() >= p+8 { + let mut b=[0u8;8]; b.copy_from_slice(&a[p..p+8]); + let x = i64::from_le_bytes(b) as f64; + let r = x.round(); + let need = 4+4+8; if *out_len < need { *out_len = need; return E_SHORT; } + let mut buf = tlv::header(1); + // encode f64 (tag=5) + buf.push(5); buf.push(0); buf.push(8); buf.push(0); buf.extend_from_slice(&r.to_le_bytes()); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out, buf.len()); *out_len = buf.len(); + return OK; + } + } + E_ARGS +} diff --git a/plugins/nyash-net-plugin/nyash_box.toml b/plugins/nyash-net-plugin/nyash_box.toml new file mode 100644 index 00000000..cf4593f9 --- /dev/null +++ b/plugins/nyash-net-plugin/nyash_box.toml @@ -0,0 +1,160 @@ +[box] +name = "Nyash Net Plugin" +version = "0.2.0" +description = "HTTP/TCP networking boxes" +author = "Nyash Team" + +[provides] +boxes = [ + "HttpServerBox", "HttpClientBox", "HttpResponseBox", "HttpRequestBox", + "SocketServerBox", "SocketClientBox", "SocketConnBox" +] + +[HttpServerBox] +type_id = 20 +[HttpServerBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[HttpServerBox.methods.start] +id = 1 +args = [ { name = "port", type = "i64" } ] +returns = { type = "void" } +returns_result = true +[HttpServerBox.methods.stop] +id = 2 +args = [] +returns = { type = "void" } +returns_result = true +[HttpServerBox.methods.accept] +id = 3 +args = [] +returns = { type = "box" } +returns_result = true + +[HttpClientBox] +type_id = 23 +[HttpClientBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[HttpClientBox.methods.get] +id = 1 +args = [ { name = "url", type = "string" } ] +returns = { type = "box" } +returns_result = true +[HttpClientBox.methods.post] +id = 2 +args = [ { name = "url", type = "string" }, { name = "body", type = "string" } ] +returns = { type = "box" } +returns_result = true + +[HttpResponseBox] +type_id = 22 +[HttpResponseBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[HttpResponseBox.methods.setStatus] +id = 1 +args = [ { name = "status", type = "i64" } ] +returns = { type = "void" } +[HttpResponseBox.methods.setHeader] +id = 2 +args = [ { name = "key", type = "string" }, { name = "value", type = "string" } ] +returns = { type = "void" } +[HttpResponseBox.methods.write] +id = 3 +args = [ { name = "body", type = "string" } ] +returns = { type = "void" } +[HttpResponseBox.methods.readBody] +id = 4 +args = [] +returns = { type = "string" } +[HttpResponseBox.methods.getStatus] +id = 5 +args = [] +returns = { type = "i64" } +[HttpResponseBox.methods.getHeader] +id = 6 +args = [ { name = "key", type = "string" } ] +returns = { type = "string" } + +[HttpRequestBox] +type_id = 21 +[HttpRequestBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[HttpRequestBox.methods.path] +id = 1 +args = [] +returns = { type = "string" } +[HttpRequestBox.methods.readBody] +id = 2 +args = [] +returns = { type = "string" } +[HttpRequestBox.methods.respond] +id = 3 +args = [ { name = "resp", type = "box" } ] +returns = { type = "void" } + +[SocketServerBox] +type_id = 30 +[SocketServerBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[SocketServerBox.methods.bind] +id = 1 +args = [ { name = "port", type = "i64" } ] +returns = { type = "void" } +[SocketServerBox.methods.accept] +id = 2 +args = [] +returns = { type = "box" } + +[SocketClientBox] +type_id = 32 +[SocketClientBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[SocketClientBox.methods.connect] +id = 1 +args = [ { name = "host", type = "string" }, { name = "port", type = "i64" } ] +returns = { type = "void" } +[SocketClientBox.methods.send] +id = 2 +args = [ { name = "data", type = "string" } ] +returns = { type = "void" } +[SocketClientBox.methods.receive] +id = 3 +args = [] +returns = { type = "string" } +[SocketClientBox.methods.close] +id = 4 +args = [] +returns = { type = "void" } + +[SocketConnBox] +type_id = 31 +[SocketConnBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } +[SocketConnBox.methods.send] +id = 1 +args = [ { name = "data", type = "string" } ] +returns = { type = "void" } +[SocketConnBox.methods.recv] +id = 2 +args = [] +returns = { type = "string" } +[SocketConnBox.methods.close] +id = 3 +args = [] +returns = { type = "void" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_net_plugin.dll" +linux = "target/release/libnyash_net_plugin.so" +macos = "target/release/libnyash_net_plugin.dylib" + diff --git a/plugins/nyash-python-plugin/nyash_box.toml b/plugins/nyash-python-plugin/nyash_box.toml new file mode 100644 index 00000000..6e433616 --- /dev/null +++ b/plugins/nyash-python-plugin/nyash_box.toml @@ -0,0 +1,62 @@ +[box] +name = "Nyash Python Plugin" +version = "0.1.0" +description = "CPython runtime and object interop" +author = "Nyash Team" + +[provides] +boxes = ["PyRuntimeBox", "PyObjectBox"] + +[PyRuntimeBox] +type_id = 40 + +[PyRuntimeBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[PyRuntimeBox.methods.eval] +id = 1 +args = [ { name = "code", type = "string" } ] +returns = { type = "box" } + +[PyRuntimeBox.methods.import] +id = 2 +args = [ { name = "name", type = "string" } ] +returns = { type = "box" } + +[PyObjectBox] +type_id = 41 + +[PyObjectBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[PyObjectBox.methods.getattr] +id = 1 +args = [ { name = "name", type = "string" } ] +returns = { type = "box" } + +[PyObjectBox.methods.call] +id = 2 +args = [ { name = "args", type = "varargs" } ] +returns = { type = "box" } + +[PyObjectBox.methods.str] +id = 3 +args = [] +returns = { type = "string" } + +[PyObjectBox.methods.callKw] +id = 5 +args = [ { name = "kwargs", type = "dict" } ] +returns = { type = "box" } + +[implementation] +ffi_version = 1 +thread_safe = false + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_python_plugin.dll" +linux = "target/release/libnyash_python_plugin.so" +macos = "target/release/libnyash_python_plugin.dylib" + diff --git a/plugins/nyash-string-plugin/nyash_box.toml b/plugins/nyash-string-plugin/nyash_box.toml new file mode 100644 index 00000000..fc48f1cb --- /dev/null +++ b/plugins/nyash-string-plugin/nyash_box.toml @@ -0,0 +1,30 @@ +[box] +name = "StringBox" +version = "1.0.0" +description = "String operations Box" +author = "Nyash Team" + +[provides] +boxes = ["StringBox"] + +[StringBox] +type_id = 10 + +[StringBox.lifecycle] +birth = { id = 0 } +fini = { id = 4294967295 } + +[StringBox.methods.length] +id = 1 +args = [] +returns = { type = "i64" } + +[implementation] +ffi_version = 1 +thread_safe = true + +[artifacts] +windows = "target/x86_64-pc-windows-msvc/release/nyash_string_plugin.dll" +linux = "target/release/libnyash_string_plugin.so" +macos = "target/release/libnyash_string_plugin.dylib" + diff --git a/src/interpreter/delegation.rs b/src/interpreter/delegation.rs index f4c7e589..2eb94480 100644 --- a/src/interpreter/delegation.rs +++ b/src/interpreter/delegation.rs @@ -365,7 +365,10 @@ impl NyashInterpreter { }); } - let math_box = MathBox::new(); + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("MathBox", &[]) { return Ok(Box::new(VoidBox::new())); } + } + let _math_box = MathBox::new(); Ok(Box::new(VoidBox::new())) } // 他のビルトインBoxは必要に応じて追加 diff --git a/src/interpreter/expressions/builtins.rs b/src/interpreter/expressions/builtins.rs index dcc6eb66..ebfa5b9a 100644 --- a/src/interpreter/expressions/builtins.rs +++ b/src/interpreter/expressions/builtins.rs @@ -13,6 +13,10 @@ impl NyashInterpreter { /// 🔥 ビルトインBoxのメソッド呼び出し pub(super) fn execute_builtin_box_method(&mut self, parent: &str, method: &str, _current_instance: Box, arguments: &[ASTNode]) -> Result, RuntimeError> { + // Strict plugin-only mode: disallow builtin paths + if std::env::var("NYASH_PLUGIN_ONLY").ok().as_deref() == Some("1") { + return Err(RuntimeError::InvalidOperation { message: format!("Builtin path disabled: {}.{}, use plugin invoke", parent, method) }); + } // 🌟 Phase 8.9: birth method support for builtin boxes if method == "birth" { @@ -42,6 +46,13 @@ impl NyashInterpreter { self.execute_map_method(&map_box, method, arguments) } "MathBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("MathBox", &[]) { + // Note: execute_math_method expects builtin MathBox; plugin path should route via VM/BoxCall in new pipeline. + // Here we simply return void; method paths should prefer plugin invoke in VM. + return Ok(Box::new(VoidBox::new())); + } + } let math_box = MathBox::new(); self.execute_math_method(&math_box, method, arguments) } @@ -57,22 +68,41 @@ impl NyashInterpreter { self.execute_file_method(&file_box, method, arguments) } "ConsoleBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("ConsoleBox", &[]) { + return Ok(Box::new(VoidBox::new())); + } + } let console_box = ConsoleBox::new(); self.execute_console_method(&console_box, method, arguments) } "TimeBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("TimeBox", &[]) { + return Ok(Box::new(VoidBox::new())); + } + } let time_box = TimeBox::new(); self.execute_time_method(&time_box, method, arguments) } "RandomBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("RandomBox", &[]) { return Ok(Box::new(VoidBox::new())); } + } let random_box = RandomBox::new(); self.execute_random_method(&random_box, method, arguments) } "DebugBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("DebugBox", &[]) { return Ok(Box::new(VoidBox::new())); } + } let debug_box = DebugBox::new(); self.execute_debug_method(&debug_box, method, arguments) } "SoundBox" => { + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("SoundBox", &[]) { return Ok(Box::new(VoidBox::new())); } + } let sound_box = SoundBox::new(); self.execute_sound_method(&sound_box, method, arguments) } @@ -158,7 +188,9 @@ impl NyashInterpreter { message: format!("MathBox.birth() expects 0 arguments, got {}", arg_values.len()), }); } - + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(_b) = reg.create_box("MathBox", &[]) { return Ok(Box::new(VoidBox::new())); } + } let _math_box = MathBox::new(); Ok(Box::new(VoidBox::new())) } diff --git a/src/interpreter/objects_non_basic_constructors.rs b/src/interpreter/objects_non_basic_constructors.rs index ba1cf0e3..46200040 100644 --- a/src/interpreter/objects_non_basic_constructors.rs +++ b/src/interpreter/objects_non_basic_constructors.rs @@ -8,6 +8,7 @@ use crate::boxes::math_box::MathBox; use crate::boxes::random_box::RandomBox; use crate::boxes::sound_box::SoundBox; use crate::boxes::debug_box::DebugBox; +use crate::box_factory::BoxFactory; impl Interpreter { /// Create non-basic type boxes (MathBox, ConsoleBox, GUI/Network boxes, etc.) @@ -18,58 +19,61 @@ impl Interpreter { ) -> Result, RuntimeError> { match class { "MathBox" => { - // MathBoxは引数なしで作成 if !arguments.is_empty() { - return Err(RuntimeError::InvalidOperation { - message: format!("MathBox constructor expects 0 arguments, got {}", arguments.len()), - }); + return Err(RuntimeError::InvalidOperation { message: format!("MathBox constructor expects 0 arguments, got {}", arguments.len()) }); } - let math_box = Box::new(MathBox::new()) as Box; - return Ok(math_box); + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("MathBox", &[]) { return Ok(b); } + } + // fallback to builtin + return Ok(Box::new(MathBox::new()) as Box); } "ConsoleBox" => { - // ConsoleBoxは引数なしで作成 + // ConsoleBoxは引数なしで作成(可能なら統一レジストリ経由でプラグイン優先) if !arguments.is_empty() { return Err(RuntimeError::InvalidOperation { message: format!("ConsoleBox constructor expects 0 arguments, got {}", arguments.len()), }); } - let console_box = Box::new(crate::box_trait::ConsoleBox::new()) as Box; - return Ok(console_box); + // Delegate to unified registry so env-based plugin overrides apply consistently + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("ConsoleBox", &[]) { + return Ok(b); + } + } + // Fallback to builtin mock if registry path failed + return Ok(Box::new(crate::box_trait::ConsoleBox::new()) as Box); } "RandomBox" => { - // RandomBoxは引数なしで作成 if !arguments.is_empty() { - return Err(RuntimeError::InvalidOperation { - message: format!("RandomBox constructor expects 0 arguments, got {}", arguments.len()), - }); + return Err(RuntimeError::InvalidOperation { message: format!("RandomBox constructor expects 0 arguments, got {}", arguments.len()) }); } - let random_box = Box::new(RandomBox::new()) as Box; - return Ok(random_box); + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("RandomBox", &[]) { return Ok(b); } + } + return Ok(Box::new(RandomBox::new()) as Box); } "SoundBox" => { - // SoundBoxは引数なしで作成 if !arguments.is_empty() { - return Err(RuntimeError::InvalidOperation { - message: format!("SoundBox constructor expects 0 arguments, got {}", arguments.len()), - }); + return Err(RuntimeError::InvalidOperation { message: format!("SoundBox constructor expects 0 arguments, got {}", arguments.len()) }); } - let sound_box = Box::new(SoundBox::new()) as Box; - return Ok(sound_box); + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("SoundBox", &[]) { return Ok(b); } + } + return Ok(Box::new(SoundBox::new()) as Box); } "DebugBox" => { - // DebugBoxは引数なしで作成 if !arguments.is_empty() { - return Err(RuntimeError::InvalidOperation { - message: format!("DebugBox constructor expects 0 arguments, got {}", arguments.len()), - }); + return Err(RuntimeError::InvalidOperation { message: format!("DebugBox constructor expects 0 arguments, got {}", arguments.len()) }); } - let debug_box = Box::new(DebugBox::new()) as Box; - return Ok(debug_box); + if let Ok(reg) = self.runtime.box_registry.lock() { + if let Ok(b) = reg.create_box("DebugBox", &[]) { return Ok(b); } + } + return Ok(Box::new(DebugBox::new()) as Box); } _ => { diff --git a/src/runtime/nyash_runtime.rs b/src/runtime/nyash_runtime.rs index dba9cef9..04d006e2 100644 --- a/src/runtime/nyash_runtime.rs +++ b/src/runtime/nyash_runtime.rs @@ -92,7 +92,13 @@ fn create_default_registry() -> Arc> { fn create_registry_with_groups(groups: BuiltinGroups) -> Arc> { let mut registry = UnifiedBoxRegistry::new(); - registry.register(Arc::new(BuiltinBoxFactory::new_with_groups(groups))); + // Optional: disable builtin boxes entirely to flush out conflicts + let disable_builtins = std::env::var("NYASH_DISABLE_BUILTINS").ok().as_deref() == Some("1"); + if !disable_builtins { + registry.register(Arc::new(BuiltinBoxFactory::new_with_groups(groups))); + } else { + eprintln!("[UnifiedRegistry] Builtin boxes disabled via NYASH_DISABLE_BUILTINS=1"); + } #[cfg(feature = "plugins")] { registry.register(Arc::new(PluginBoxFactory::new())); diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 796ac375..89a3d76f 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -44,6 +44,16 @@ mod enabled { finalized: std::sync::atomic::AtomicBool, } + // Loaded box spec from plugins//nyash_box.toml + #[derive(Debug, Clone, Default)] + struct LoadedBoxSpec { + type_id: Option, + methods: HashMap, + fini_method_id: Option, + } + #[derive(Debug, Clone, Copy)] + struct MethodSpec { method_id: u32, returns_result: bool } + impl Drop for PluginHandleInner { fn drop(&mut self) { // Finalize exactly once when the last shared handle is dropped @@ -208,6 +218,8 @@ impl PluginBoxV2 { /// Singleton instances: (lib_name, box_type) -> shared handle singletons: RwLock>>, + /// Loaded per-plugin box specs from nyash_box.toml: (lib_name, box_type) -> spec + box_specs: RwLock>, } impl PluginLoaderV2 { @@ -230,6 +242,7 @@ impl PluginBoxV2 { config: None, config_path: None, singletons: RwLock::new(HashMap::new()), + box_specs: RwLock::new(HashMap::new()), } } @@ -275,20 +288,69 @@ impl PluginBoxV2 { // Synthesize a LibraryDefinition from plugin spec (nyash_box.toml) if present; otherwise minimal let mut boxes: Vec = Vec::new(); let spec_path = std::path::Path::new(root).join("nyash_box.toml"); + // Optional artifact path from spec + let mut artifact_override: Option = None; if let Ok(txt) = std::fs::read_to_string(&spec_path) { if let Ok(val) = txt.parse::() { if let Some(prov) = val.get("provides").and_then(|t| t.get("boxes")).and_then(|a| a.as_array()) { for it in prov.iter() { if let Some(s) = it.as_str() { boxes.push(s.to_string()); } } } + // Artifacts section: choose OS-specific path template if provided + if let Some(arts) = val.get("artifacts").and_then(|t| t.as_table()) { + let key = if cfg!(target_os = "windows") { "windows" } else if cfg!(target_os = "macos") { "macos" } else { "linux" }; + if let Some(p) = arts.get(key).and_then(|v| v.as_str()) { + artifact_override = Some(p.to_string()); + } + } + // Build per-box specs + for bname in &boxes { + let mut spec = LoadedBoxSpec::default(); + if let Some(tb) = val.get(bname) { + if let Some(tid) = tb.get("type_id").and_then(|v| v.as_integer()) { spec.type_id = Some(tid as u32); } + if let Some(lc) = tb.get("lifecycle") { + if let Some(fini) = lc.get("fini").and_then(|m| m.get("id")).and_then(|v| v.as_integer()) { spec.fini_method_id = Some(fini as u32); } + } + if let Some(mtbl) = tb.get("methods").and_then(|t| t.as_table()) { + for (mname, md) in mtbl.iter() { + if let Some(mid) = md.get("id").and_then(|v| v.as_integer()) { + let rr = md.get("returns_result").and_then(|v| v.as_bool()).unwrap_or(false); + spec.methods.insert(mname.clone(), MethodSpec { method_id: mid as u32, returns_result: rr }); + } + } + } + } + if spec.type_id.is_some() || !spec.methods.is_empty() { + self.box_specs.write().unwrap().insert((plugin_name.clone(), bname.clone()), spec); + } + } } } - // Path heuristic: use "/" (extension will be adapted by resolver) - let synth_path = std::path::Path::new(root).join(plugin_name).to_string_lossy().to_string(); + // Path heuristic: use artifact override if present, else "/" + let synth_path = if let Some(p) = artifact_override { + let jp = std::path::Path::new(root).join(p); + jp.to_string_lossy().to_string() + } else { + std::path::Path::new(root).join(plugin_name).to_string_lossy().to_string() + }; let lib_def = LibraryDefinition { boxes: boxes.clone(), path: synth_path }; if let Err(e) = self.load_plugin(plugin_name, &lib_def) { eprintln!("Warning: Failed to load plugin {} from [plugins]: {:?}", plugin_name, e); } } + // Strict validation: central [box_types] vs plugin spec type_id + let strict = std::env::var("NYASH_PLUGIN_STRICT").ok().as_deref() == Some("1"); + if !config.box_types.is_empty() { + for ((lib, bname), spec) in self.box_specs.read().unwrap().iter() { + if let Some(cid) = config.box_types.get(bname) { + if let Some(tid) = spec.type_id { + if tid != *cid { + eprintln!("[PluginLoaderV2] type_id mismatch for {} (plugin={}): central={}, spec={}", bname, lib, cid, tid); + if strict { return Err(BidError::PluginError); } + } + } + } + } + } // Pre-birth singletons configured in nyash.toml let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml"); let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?; @@ -306,6 +368,16 @@ impl PluginBoxV2 { Ok(()) } + fn find_lib_name_for_box(&self, box_type: &str) -> Option { + if let Some(cfg) = &self.config { + if let Some((name, _)) = cfg.find_library_for_box(box_type) { return Some(name.to_string()); } + } + for ((lib, b), _) in self.box_specs.read().unwrap().iter() { + if b == box_type { return Some(lib.clone()); } + } + None + } + /// Ensure a singleton handle is created and stored fn ensure_singleton_handle(&self, lib_name: &str, box_type: &str) -> BidResult<()> { // Fast path: already present @@ -319,8 +391,13 @@ impl PluginBoxV2 { let config = self.config.as_ref().ok_or(BidError::PluginError)?; let plugins = self.plugins.read().unwrap(); let plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?; - let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; - let type_id = box_conf.type_id; + // Prefer spec-loaded type_id + let type_id = if let Some(spec) = self.box_specs.read().unwrap().get(&(lib_name.to_string(), box_type.to_string())) { + spec.type_id.unwrap_or_else(|| config.box_types.get(box_type).copied().unwrap_or(0)) + } else { + let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; + box_conf.type_id + }; // Call birth let mut output_buffer = vec![0u8; 1024]; let mut output_len = output_buffer.len(); @@ -330,7 +407,12 @@ impl PluginBoxV2 { }; if birth_result != 0 || output_len < 4 { return Err(BidError::PluginError); } let instance_id = u32::from_le_bytes([output_buffer[0], output_buffer[1], output_buffer[2], output_buffer[3]]); - let fini_id = box_conf.methods.get("fini").map(|m| m.method_id); + let fini_id = if let Some(spec) = self.box_specs.read().unwrap().get(&(lib_name.to_string(), box_type.to_string())) { + spec.fini_method_id + } else { + let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; + box_conf.methods.get("fini").map(|m| m.method_id) + }; let handle = std::sync::Arc::new(PluginHandleInner { type_id, invoke_fn: plugin.invoke_fn, @@ -372,12 +454,16 @@ impl PluginBoxV2 { fn resolve_method_id_from_file(&self, box_type: &str, method_name: &str) -> BidResult { let config = self.config.as_ref().ok_or(BidError::PluginError)?; - let (lib_name, _lib_def) = config.find_library_for_box(box_type) - .ok_or(BidError::InvalidType)?; + let lib_name = self.find_lib_name_for_box(box_type).ok_or(BidError::InvalidType)?; + // Prefer spec-loaded methods + if let Some(spec) = self.box_specs.read().unwrap().get(&(lib_name.clone(), box_type.to_string())) { + if let Some(m) = spec.methods.get(method_name) { return Ok(m.method_id); } + } + // Fallback to central nyash.toml nested box config let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml"); let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?; let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| BidError::PluginError)?; - let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; + let box_conf = config.get_box_config(&lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; let method = box_conf.methods.get(method_name).ok_or_else(|| { eprintln!("[PluginLoaderV2] Method '{}' not found for box '{}' in {}", method_name, box_type, cfg_path); eprintln!("[PluginLoaderV2] Available methods: {:?}", box_conf.methods.keys().collect::>()); @@ -398,21 +484,34 @@ impl PluginBoxV2 { let method_id = self.resolve_method_id_from_file(box_type, method_name)?; // Find plugin and type_id let config = self.config.as_ref().ok_or(BidError::PluginError)?; - let (lib_name, _lib_def) = config.find_library_for_box(box_type).ok_or(BidError::InvalidType)?; + let lib_name = self.find_lib_name_for_box(box_type).ok_or(BidError::InvalidType)?; let plugins = self.plugins.read().unwrap(); - let plugin = plugins.get(lib_name).ok_or(BidError::PluginError)?; + let plugin = plugins.get(&lib_name).ok_or(BidError::PluginError)?; let cfg_path = self.config_path.as_ref().map(|s| s.as_str()).unwrap_or("nyash.toml"); let toml_content = std::fs::read_to_string(cfg_path).map_err(|_| BidError::PluginError)?; let toml_value: toml::Value = toml::from_str(&toml_content).map_err(|_| BidError::PluginError)?; - let box_conf = config.get_box_config(lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; - let type_id = box_conf.type_id; - let returns_result = box_conf.methods.get(method_name).map(|m| m.returns_result).unwrap_or(false); + // Prefer spec-loaded type_id/method returns_result + let (type_id, returns_result) = if let Some(spec) = self.box_specs.read().unwrap().get(&(lib_name.clone(), box_type.to_string())) { + let tid = spec.type_id.unwrap_or_else(|| config.box_types.get(box_type).copied().unwrap_or(0)); + let rr = spec.methods.get(method_name).map(|m| m.returns_result).unwrap_or(false); + (tid, rr) + } else { + let box_conf = config.get_box_config(&lib_name, box_type, &toml_value).ok_or(BidError::InvalidType)?; + let rr = box_conf.methods.get(method_name).map(|m| m.returns_result).unwrap_or(false); + (box_conf.type_id, rr) + }; eprintln!("[PluginLoaderV2] Invoke {}.{}: resolving and encoding args (argc={})", box_type, method_name, args.len()); // TLV args: encode using BID-1 style (u16 ver, u16 argc, then entries) let tlv_args = { let mut buf = crate::runtime::plugin_ffi_common::encode_tlv_header(args.len() as u16); // Validate against nyash.toml method args schema if present - let expected_args = box_conf.methods.get(method_name).and_then(|m| m.args.clone()); + let expected_args = if self.box_specs.read().unwrap().get(&(lib_name.clone(), box_type.to_string())).is_some() { + None + } else { + config + .get_box_config(&lib_name, box_type, &toml_value) + .and_then(|bc| bc.methods.get(method_name).and_then(|m| m.args.clone())) + }; if let Some(exp) = expected_args.as_ref() { if exp.len() != args.len() { eprintln!( @@ -599,6 +698,12 @@ impl PluginBoxV2 { let val: Box = Box::new(crate::box_trait::BoolBox::new(b)); if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box) } else { Some(val) } } + 5 if size == 8 => { // F64 + if let Some(f) = crate::runtime::plugin_ffi_common::decode::f64(payload) { + let val: Box = Box::new(crate::boxes::math_box::FloatBox::new(f)); + if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box) } else { Some(val) } + } else { None } + } 8 if size == 8 => { // Handle -> PluginBoxV2 let mut t = [0u8;4]; t.copy_from_slice(&payload[0..4]); let mut i = [0u8;4]; i.copy_from_slice(&payload[4..8]); @@ -838,27 +943,42 @@ impl PluginBoxV2 { eprintln!("🔍 Plugin loaded successfully"); - // Get type_id from config - read actual nyash.toml content - eprintln!("🔍 Reading nyash.toml for type configuration..."); - let (type_id, fini_method_id) = if let Ok(toml_content) = std::fs::read_to_string(cfg_path) { - eprintln!("🔍 nyash.toml read successfully"); - if let Ok(toml_value) = toml::from_str::(&toml_content) { - eprintln!("🔍 nyash.toml parsed successfully"); - if let Some(box_config) = config.get_box_config(lib_name, box_type, &toml_value) { - eprintln!("🔍 Found box config for {} with type_id: {}", box_type, box_config.type_id); - let fini_id = box_config.methods.get("fini").map(|m| m.method_id); - (box_config.type_id, fini_id) + // Resolve type_id/fini: prefer per-plugin spec (nyash_box.toml), fallback to central nyash.toml + let (type_id, fini_method_id) = if let Some(spec) = self.box_specs.read().unwrap().get(&(lib_name.to_string(), box_type.to_string())) { + // Prefer explicit spec values; if missing, fallback to central [box_types] and no fini + let tid = spec + .type_id + .or_else(|| config.box_types.get(box_type).copied()) + .ok_or_else(|| { + eprintln!( + "No type_id found for {} (plugin spec missing and central [box_types] not set)", + box_type + ); + BidError::InvalidType + })?; + (tid, spec.fini_method_id) + } else { + eprintln!("🔍 Reading nyash.toml for type configuration..."); + if let Ok(toml_content) = std::fs::read_to_string(cfg_path) { + eprintln!("🔍 nyash.toml read successfully"); + if let Ok(toml_value) = toml::from_str::(&toml_content) { + eprintln!("🔍 nyash.toml parsed successfully"); + if let Some(box_config) = config.get_box_config(lib_name, box_type, &toml_value) { + eprintln!("🔍 Found box config for {} with type_id: {}", box_type, box_config.type_id); + let fini_id = box_config.methods.get("fini").map(|m| m.method_id); + (box_config.type_id, fini_id) + } else { + eprintln!("No type configuration for {} in {}", box_type, lib_name); + return Err(BidError::InvalidType); + } } else { - eprintln!("No type configuration for {} in {}", box_type, lib_name); - return Err(BidError::InvalidType); + eprintln!("Failed to parse nyash.toml"); + return Err(BidError::PluginError); } } else { - eprintln!("Failed to parse nyash.toml"); + eprintln!("Failed to read nyash.toml"); return Err(BidError::PluginError); } - } else { - eprintln!("Failed to read nyash.toml"); - return Err(BidError::PluginError); }; // Call birth constructor (method_id = 0) via TLV encoding diff --git a/src/runtime/unified_registry.rs b/src/runtime/unified_registry.rs index e474885e..3019b02d 100644 --- a/src/runtime/unified_registry.rs +++ b/src/runtime/unified_registry.rs @@ -18,8 +18,13 @@ pub fn init_global_unified_registry() { GLOBAL_REGISTRY.get_or_init(|| { let mut registry = UnifiedBoxRegistry::new(); - // Register built-in Box factory (highest priority) - registry.register(Arc::new(BuiltinBoxFactory::new())); + // Register built-in Box factory (highest priority) unless disabled + let disable_builtins = std::env::var("NYASH_DISABLE_BUILTINS").ok().as_deref() == Some("1"); + if !disable_builtins { + registry.register(Arc::new(BuiltinBoxFactory::new())); + } else { + eprintln!("[UnifiedRegistry] Builtin boxes disabled via NYASH_DISABLE_BUILTINS=1"); + } // Register plugin Box factory (lowest priority) #[cfg(feature = "plugins")] diff --git a/string_len_app b/string_len_app new file mode 100644 index 00000000..fca8dcd4 Binary files /dev/null and b/string_len_app differ diff --git a/tools/smoke_plugins.sh b/tools/smoke_plugins.sh new file mode 100644 index 00000000..1f66547b --- /dev/null +++ b/tools/smoke_plugins.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [[ ! -x "$BIN" ]]; then + echo "[smoke] building nyash (release, cranelift-jit)..." >&2 + (cd "$ROOT_DIR" && cargo build --release --features cranelift-jit >/dev/null) +fi + +# Build required plugins (release) +build_plugin() { + local dir=$1 + if [[ -d "$ROOT_DIR/$dir" ]]; then + echo "[smoke] building $dir ..." >&2 + (cd "$ROOT_DIR/$dir" && cargo build --release >/dev/null) + fi +} + +build_plugin plugins/nyash-python-plugin +build_plugin plugins/nyash-integer-plugin +build_plugin plugins/nyash-console-plugin +build_plugin plugins/nyash-math-plugin + +export NYASH_CLI_VERBOSE=1 +export NYASH_PLUGIN_STRICT=1 +export NYASH_USE_PLUGIN_BUILTINS=1 +export NYASH_PLUGIN_OVERRIDE_TYPES="ArrayBox,MapBox,ConsoleBox" + +run_case() { + local name=$1 + local file=$2 + echo "[smoke] case=$name file=$file" >&2 + "$BIN" --backend vm "$ROOT_DIR/$file" >/dev/null + echo "[smoke] ok: $name" >&2 +} + +# Core plugin demos +run_case py_math_sqrt_demo examples/py_math_sqrt_demo.nyash +run_case integer_plugin_demo examples/integer_plugin_demo.nyash +run_case console_demo examples/console_demo.nyash +run_case math_time_demo examples/math_time_demo.nyash + +echo "[smoke] all green" >&2 + +# Second pass: disable builtins and re-run key cases +echo "[smoke] second pass with NYASH_DISABLE_BUILTINS=1" >&2 +NYASH_DISABLE_BUILTINS=1 \ + "$BIN" --backend vm "$ROOT_DIR/examples/console_demo.nyash" >/dev/null +NYASH_DISABLE_BUILTINS=1 \ + "$BIN" --backend vm "$ROOT_DIR/examples/math_time_demo.nyash" >/dev/null +echo "[smoke] all green (builtins disabled)" >&2