diff --git a/docs/development/box_identity_and_copy_semantics.md b/docs/development/box_identity_and_copy_semantics.md new file mode 100644 index 00000000..26759fa0 --- /dev/null +++ b/docs/development/box_identity_and_copy_semantics.md @@ -0,0 +1,72 @@ +Box Identity and Copy Semantics (Nyash) + +Context +- In Nyash, everything is a Box. Some boxes carry identity (external handles, stateful resources), while others are pure values. +- Picking clone_box vs share_box is critical: getting it wrong can silently fork state or break handle identity (e.g., plugin/socket/file handles). + +Key Terms +- Identity Box: Represents an external or stateful handle where the instance_id (or similar) must remain stable across uses. Examples: PluginBoxV2 (all plugin boxes), Socket boxes, FileBox, DB handles. +- Value Box: Pure data containers where cloning yields an equivalent value. Examples: StringBox, IntegerBox, JSONBox, ArrayBox/MapBox (when used as data). + +Rules of Thumb +- When preserving identity matters (handle/connection/descriptor): use share_box. +- When passing or returning pure values: use clone_box (or deep copy APIs) so callers can freely mutate their copy. +- Do not call clone_box on identity boxes during common control flows like method dispatch, result unwrapping, or variable assignment unless you’re explicitly birthing a new instance. + +Where mistakes commonly happen +1) Result unwrap (ResultBox.get_value) + - Wrong: always clone_box() — duplicates plugin handles and changes instance_id. + - Right: if value is PluginBoxV2 (or any identity box), return share_box(); else clone_box(). + +2) Method dispatch (receiver preparation) + - Wrong: always clone the receiver box before method call. + - Right: if the VM stores BoxRef(Arc), use Arc::share_box() for the receiver; only clone for value boxes. + +3) Environment/variable storage + - Wrong: eagerly clone_box() on set/assign. + - Right: keep as BoxRef (Arc) where semantics require identity; only copy for value semantics. + +4) Returning plugin results + - Ensure loader returns PluginBoxV2 with the exact instance_id provided by the plugin, wrapped as BoxRef. Avoid any implicit clone. + +Concrete patterns +- In ResultBox.get_value(): + - if val.is::() -> return val.share_box() + - else -> val.clone_box() + +- In VMValue conversions: + - VMValue::from_nyash_box: store as BoxRef(Arc) for identity-bearing boxes (default safe choice). + - VMValue::to_nyash_box: for BoxRef -> share_box(). + +- In plugin method dispatch (PluginBoxV2): + - Identify receiver as PluginBoxV2 and pass its instance_id to FFI directly (avoid cloning receiver). + +Anti-patterns to avoid +- Using clone_box blindly in: + - Result unwrap paths + - Method receiver preparation + - Field access and variable bindings for identity boxes + +Design guidance +- Consider tagging boxes with an identity flag: + - Add BoxCore::is_identity() -> bool (default false; override to true for PluginBoxV2, Socket/File/DB boxes). + - Provide a helper: clone_or_share_by_semantics(box: &dyn NyashBox) that calls share for identity and clone for values. +- Add debug assertions/logs in dev builds when a PluginBoxV2’s instance_id unexpectedly changes across a single logical flow (indicative of unintended clone). + +Testing checklist +- Log instance_id for plugin boxes at: + - client.get/post result (resp_id) + - ResultBox.get_value return (should match) + - Method dispatch receiver (should match) + - readBody()/write logs (should match) +- For e2e tests, assert that write/mirror/read use the same response id. + +Migrations for existing code +- Update ResultBox.get_value to share PluginBoxV2. +- Audit call sites that do clone_box() before method dispatch or storage; prefer BoxRef + share for identity boxes. +- Ensure plugin loader returns PluginBoxV2 as BoxRef, not cloned. + +Future improvements +- Static lint: forbid clone_box on types that implement BoxCore::is_identity() == true in critical paths (unwrap, dispatch, assign). +- Provide a macro/helper to explicitly mark intent: share!(x), clone_value!(x) to make code review easier. + diff --git a/docs/development/current/CURRENT_TASK.md b/docs/development/current/CURRENT_TASK.md index 63194aa1..8d30df63 100644 --- a/docs/development/current/CURRENT_TASK.md +++ b/docs/development/current/CURRENT_TASK.md @@ -23,20 +23,25 @@ - デリゲーションテスト: ❓ 未実装の可能性 - VMテスト: ❌ 失敗(VMはまだプラグインBox未対応) -### 🎯 次のタスク (Phase 9.78b) +### 🎯 次のタスク(MIR→VM チェック / 命令最適化) -#### Step 3: BoxFactory dyn化(優先度: 高) -- 現在: `HashMap Arc>>` -- 目標: `HashMap>` -- 利点: プラグインBoxもVMで統一処理可能 +1) MIR→VM 変換の健全性チェック(短期) +- 現行 MIR 命令 → VM 命令のマッピング表を作成 +- E2E/サンプルを VM バックエンドで実行し、MIR→VM 変換ログを収集 +- 例外系・Result正規化(returns_result)の経路を VM で確認 -#### Step 4: グローバル排除 -- `get_global_registry()` → `runtime.registry` -- `get_global_loader_v2()` → `runtime.plugin_loader` +2) 命令使用実績の計測(短期) +- 実際に使用されている VM 命令を計測(実行ログ/カウンタ) +- 現行宣言33命令 → 実使用コア命令の抽出 -#### Step 5: SharedState分解 -- 巨大なSharedState構造体を分割 -- Box管理、メソッド管理、スコープ管理を分離 +3) 命令セットのダイエット検討(中期) +- 目標: 26命令(docsの合意値) +- 代替可能/統合可能な命令の提案と互換性影響の洗い出し +- 実験フラグで26命令版のVMを試作(段階移行) + +4) ドキュメント更新(短期) +- 現状の命令一覧と目標26命令の整合(reference/architecture/) +- 計測結果(使用頻度)と統合方針を追記 ### 📝 メモ - ChatGPT5がプラグインBoxメソッド呼び出しに引数/戻り値サポートを追加 @@ -44,10 +49,10 @@ - Rustの借用チェッカーとの格闘の跡が見られる(複数回の修正) ### 🔧 推奨アクション -1. セッション再起動してBashコマンドを復活 -2. ビルド実行: `cargo build --release -j32` -3. E2Eテスト実行: `cargo test e2e_plugin_filebox --features plugins -- --show-output` -4. VMプラグイン統合の実装開始(Phase 9.78b Step 3) +1. VMバックエンドでの実行ログ化(命令発行ログ/カウンタ) +2. マッピング表作成(MIR→VM)と欠落/冗長命令の洗い出し +3. 26命令案のPoCブランチ作成→E2Eで回帰確認 +4. docs更新(命令一覧、設計意図、移行戦略) --- -最終更新: 2025年8月20日 22:45 \ No newline at end of file +最終更新: 2025年8月22日 03:30(MIR→VM/命令最適化タスク追加) diff --git a/nyash.toml b/nyash.toml index cf121bae..f85ef56b 100644 --- a/nyash.toml +++ b/nyash.toml @@ -11,29 +11,22 @@ path = "./plugins/nyash-filebox-plugin/target/release/libnyash_filebox_plugin.so boxes = ["CounterBox"] path = "./plugins/nyash-counter-plugin/target/release/libnyash_counter_plugin.so" -# 将来の拡張例: -# "libnyash_database_plugin.so" = { -# boxes = ["PostgreSQLBox", "MySQLBox", "SQLiteBox"], -# path = "./target/release/libnyash_database_plugin.so" -# } +[libraries."libnyash_net_plugin.so"] +boxes = ["HttpServerBox", "HttpClientBox", "HttpResponseBox", "HttpRequestBox", "SocketServerBox", "SocketClientBox", "SocketConnBox"] +path = "./plugins/nyash-net-plugin/target/release/libnyash_net_plugin.so" # FileBoxの型情報定義 [libraries."libnyash_filebox_plugin.so".FileBox] type_id = 6 [libraries."libnyash_filebox_plugin.so".FileBox.methods] -# 全メソッドをmethod_idと共に定義 birth = { method_id = 0 } open = { method_id = 1, args = ["path", "mode"] } read = { method_id = 2 } write = { method_id = 3, args = ["data"] } close = { method_id = 4 } fini = { method_id = 4294967295 } - -# v2.1: Box引数を受け取るメソッド宣言(FileBox: copyFrom(other: Handle)) copyFrom = { method_id = 7, args = [ { kind = "box", category = "plugin" } ] } - -# v2.2: BoxRef(Handle)を返すメソッド宣言 cloneSelf = { method_id = 8 } [libraries."libnyash_counter_plugin.so".CounterBox] @@ -46,20 +39,7 @@ inc = { method_id = 1 } get = { method_id = 2 } fini = { method_id = 4294967295 } -[plugin_paths] -# プラグインの検索パス(デフォルト) -search_paths = [ - "./target/release", - "./target/debug", - "./plugins/*/target/release", - "./plugins/*/target/debug", - "/usr/local/lib/nyash/plugins", - "~/.nyash/plugins" -] -[libraries."libnyash_net_plugin.so"] -boxes = ["HttpServerBox", "HttpRequestBox", "HttpResponseBox", "HttpClientBox", "SocketServerBox", "SocketClientBox", "SocketConnBox"] -path = "./plugins/nyash-net-plugin/target/release/libnyash_net_plugin.so" - +# HttpServerBox [libraries."libnyash_net_plugin.so".HttpServerBox] type_id = 20 @@ -70,6 +50,31 @@ stop = { method_id = 2, returns_result = true } accept = { method_id = 3, returns_result = true } fini = { method_id = 4294967295 } +# HttpClientBox +[libraries."libnyash_net_plugin.so".HttpClientBox] +type_id = 23 + +[libraries."libnyash_net_plugin.so".HttpClientBox.methods] +birth = { method_id = 0 } +get = { method_id = 1, args = ["url"], returns_result = true } +post = { method_id = 2, args = ["url", "body"], returns_result = true } +fini = { method_id = 4294967295 } + +# HttpResponseBox +[libraries."libnyash_net_plugin.so".HttpResponseBox] +type_id = 22 + +[libraries."libnyash_net_plugin.so".HttpResponseBox.methods] +birth = { method_id = 0 } +setStatus = { method_id = 1, args = ["status"] } +setHeader = { method_id = 2, args = ["key", "value"] } +write = { method_id = 3, args = ["body"] } +readBody = { method_id = 4 } +getStatus = { method_id = 5 } +getHeader = { method_id = 6, args = ["key"] } +fini = { method_id = 4294967295 } + +# HttpRequestBox [libraries."libnyash_net_plugin.so".HttpRequestBox] type_id = 21 @@ -77,67 +82,49 @@ type_id = 21 birth = { method_id = 0 } path = { method_id = 1 } readBody = { method_id = 2 } -respond = { method_id = 3, args = [ { kind = "box", category = "plugin" } ] } +respond = { method_id = 3, args = [{ kind = "box", category = "plugin" }] } fini = { method_id = 4294967295 } -[libraries."libnyash_net_plugin.so".HttpResponseBox] -type_id = 22 - -[libraries."libnyash_net_plugin.so".HttpResponseBox.methods] -birth = { method_id = 0 } -setStatus = { method_id = 1 } -setHeader = { method_id = 2 } -write = { method_id = 3 } -readBody = { method_id = 4 } -getStatus = { method_id = 5 } -getHeader = { method_id = 6, args = ["name"] } -fini = { method_id = 4294967295 } - -[libraries."libnyash_net_plugin.so".HttpClientBox] -type_id = 23 - -[libraries."libnyash_net_plugin.so".HttpClientBox.methods] -birth = { method_id = 0 } -get = { method_id = 1, returns_result = true } -post = { method_id = 2, returns_result = true } -fini = { method_id = 4294967295 } - -## ResultBox normalization enabled above for get/post - +# SocketServerBox [libraries."libnyash_net_plugin.so".SocketServerBox] type_id = 30 [libraries."libnyash_net_plugin.so".SocketServerBox.methods] birth = { method_id = 0 } -start = { method_id = 1, args = ["port"], returns_result = true } -stop = { method_id = 2, returns_result = true } -accept = { method_id = 3, returns_result = true } -acceptTimeout = { method_id = 4, args = ["timeout_ms"], returns_result = true } +bind = { method_id = 1, args = ["port"] } +accept = { method_id = 2 } fini = { method_id = 4294967295 } -# Optional: ResultBox normalization (recommendation) -# start = { method_id = 1, args = ["port"], returns_result = true } -# stop = { method_id = 2, returns_result = true } -# accept = { method_id = 3, returns_result = true } - +# SocketClientBox [libraries."libnyash_net_plugin.so".SocketClientBox] type_id = 32 [libraries."libnyash_net_plugin.so".SocketClientBox.methods] birth = { method_id = 0 } -connect = { method_id = 1, args = ["host", "port"], returns_result = true } +connect = { method_id = 1, args = ["host", "port"] } +send = { method_id = 2, args = ["data"] } +receive = { method_id = 3 } +close = { method_id = 4 } fini = { method_id = 4294967295 } -# Optional: ResultBox normalization (recommendation) -# connect = { method_id = 1, args = ["host", "port"], returns_result = true } - +# SocketConnBox [libraries."libnyash_net_plugin.so".SocketConnBox] type_id = 31 [libraries."libnyash_net_plugin.so".SocketConnBox.methods] birth = { method_id = 0 } -send = { method_id = 1 } +send = { method_id = 1, args = ["data"] } recv = { method_id = 2 } close = { method_id = 3 } -recvTimeout = { method_id = 4, args = ["timeout_ms"], returns_result = true } fini = { method_id = 4294967295 } + +[plugin_paths] +# プラグインの検索パス(デフォルト) +search_paths = [ + "./target/release", + "./target/debug", + "./plugins/*/target/release", + "./plugins/*/target/debug", + "/usr/local/lib/nyash/plugins", + "~/.nyash/plugins" +] diff --git a/nyash.toml.backup_duplicate b/nyash.toml.backup_duplicate new file mode 100644 index 00000000..04ff888d --- /dev/null +++ b/nyash.toml.backup_duplicate @@ -0,0 +1,215 @@ +# Nyash Configuration File v2 +# マルチBox型プラグイン対応 + +[libraries] +# ライブラリ定義(1つのプラグインで複数のBox型を提供可能) +[libraries."libnyash_filebox_plugin.so"] +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" + +[libraries."libnyash_net_plugin.so"] +boxes = ["HttpServerBox", "HttpClientBox", "HttpResponseBox", "HttpRequestBox", "SocketServerBox", "SocketClientBox"] +path = "./plugins/nyash-net-plugin/target/release/libnyash_net_plugin.so" + +# 将来の拡張例: +# "libnyash_database_plugin.so" = { +# boxes = ["PostgreSQLBox", "MySQLBox", "SQLiteBox"], +# path = "./target/release/libnyash_database_plugin.so" +# } + +# FileBoxの型情報定義 +[libraries."libnyash_filebox_plugin.so".FileBox] +type_id = 6 + +[libraries."libnyash_filebox_plugin.so".FileBox.methods] +# 全メソッドをmethod_idと共に定義 +birth = { method_id = 0 } +open = { method_id = 1, args = ["path", "mode"] } +read = { method_id = 2 } +write = { method_id = 3, args = ["data"] } +close = { method_id = 4 } +fini = { method_id = 4294967295 } + +# v2.1: Box引数を受け取るメソッド宣言(FileBox: copyFrom(other: Handle)) +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 +singleton = true + +[libraries."libnyash_counter_plugin.so".CounterBox.methods] +birth = { method_id = 0 } +inc = { method_id = 1 } +get = { method_id = 2 } +fini = { method_id = 4294967295 } + +# HttpServerBox +[libraries."libnyash_net_plugin.so".HttpServerBox] +type_id = 10 + +[libraries."libnyash_net_plugin.so".HttpServerBox.methods] +birth = { method_id = 0 } +start = { method_id = 1, args = ["port"], returns_result = true } +stop = { method_id = 2, returns_result = true } +accept = { method_id = 3, returns_result = true } +fini = { method_id = 4294967295 } + +# HttpClientBox +[libraries."libnyash_net_plugin.so".HttpClientBox] +type_id = 11 + +[libraries."libnyash_net_plugin.so".HttpClientBox.methods] +birth = { method_id = 0 } +get = { method_id = 1, args = ["url"] } +post = { method_id = 2, args = ["url", "body"] } +fini = { method_id = 4294967295 } + +# HttpResponseBox +[libraries."libnyash_net_plugin.so".HttpResponseBox] +type_id = 12 + +[libraries."libnyash_net_plugin.so".HttpResponseBox.methods] +birth = { method_id = 0 } +setStatus = { method_id = 1, args = ["status"] } +setHeader = { method_id = 2, args = ["key", "value"] } +write = { method_id = 3, args = ["body"] } +readBody = { method_id = 4 } +getStatus = { method_id = 5 } +getHeader = { method_id = 6, args = ["key"] } +fini = { method_id = 4294967295 } + +# HttpRequestBox +[libraries."libnyash_net_plugin.so".HttpRequestBox] +type_id = 13 + +[libraries."libnyash_net_plugin.so".HttpRequestBox.methods] +birth = { method_id = 0 } +respond = { method_id = 1, args = [{ kind = "box", category = "plugin" }] } +path = { method_id = 2 } +readBody = { method_id = 3 } +fini = { method_id = 4294967295 } + +# SocketServerBox +[libraries."libnyash_net_plugin.so".SocketServerBox] +type_id = 14 + +[libraries."libnyash_net_plugin.so".SocketServerBox.methods] +birth = { method_id = 0 } +bind = { method_id = 1, args = ["port"] } +accept = { method_id = 2 } +fini = { method_id = 4294967295 } + +# SocketClientBox +[libraries."libnyash_net_plugin.so".SocketClientBox] +type_id = 15 + +[libraries."libnyash_net_plugin.so".SocketClientBox.methods] +birth = { method_id = 0 } +connect = { method_id = 1, args = ["host", "port"] } +send = { method_id = 2, args = ["data"] } +receive = { method_id = 3 } +close = { method_id = 4 } +fini = { method_id = 4294967295 } + +[plugin_paths] +# プラグインの検索パス(デフォルト) +search_paths = [ + "./target/release", + "./target/debug", + "./plugins/*/target/release", + "./plugins/*/target/debug", + "/usr/local/lib/nyash/plugins", + "~/.nyash/plugins" +] +[libraries."libnyash_net_plugin.so"] +boxes = ["HttpServerBox", "HttpRequestBox", "HttpResponseBox", "HttpClientBox", "SocketServerBox", "SocketClientBox", "SocketConnBox"] +path = "./plugins/nyash-net-plugin/target/release/libnyash_net_plugin.so" + +[libraries."libnyash_net_plugin.so".HttpServerBox] +type_id = 20 + +[libraries."libnyash_net_plugin.so".HttpServerBox.methods] +birth = { method_id = 0 } +start = { method_id = 1, args = ["port"], returns_result = true } +stop = { method_id = 2, returns_result = true } +accept = { method_id = 3, returns_result = true } +fini = { method_id = 4294967295 } + +[libraries."libnyash_net_plugin.so".HttpRequestBox] +type_id = 21 + +[libraries."libnyash_net_plugin.so".HttpRequestBox.methods] +birth = { method_id = 0 } +path = { method_id = 1 } +readBody = { method_id = 2 } +respond = { method_id = 3, args = [ { kind = "box", category = "plugin" } ] } +fini = { method_id = 4294967295 } + +[libraries."libnyash_net_plugin.so".HttpResponseBox] +type_id = 22 + +[libraries."libnyash_net_plugin.so".HttpResponseBox.methods] +birth = { method_id = 0 } +setStatus = { method_id = 1 } +setHeader = { method_id = 2 } +write = { method_id = 3 } +readBody = { method_id = 4 } +getStatus = { method_id = 5 } +getHeader = { method_id = 6, args = ["name"] } +fini = { method_id = 4294967295 } + +[libraries."libnyash_net_plugin.so".HttpClientBox] +type_id = 23 + +[libraries."libnyash_net_plugin.so".HttpClientBox.methods] +birth = { method_id = 0 } +get = { method_id = 1, returns_result = true } +post = { method_id = 2, returns_result = true } +fini = { method_id = 4294967295 } + +## ResultBox normalization enabled above for get/post + +[libraries."libnyash_net_plugin.so".SocketServerBox] +type_id = 30 + +[libraries."libnyash_net_plugin.so".SocketServerBox.methods] +birth = { method_id = 0 } +start = { method_id = 1, args = ["port"], returns_result = true } +stop = { method_id = 2, returns_result = true } +accept = { method_id = 3, returns_result = true } +acceptTimeout = { method_id = 4, args = ["timeout_ms"], returns_result = true } +fini = { method_id = 4294967295 } + +# Optional: ResultBox normalization (recommendation) +# start = { method_id = 1, args = ["port"], returns_result = true } +# stop = { method_id = 2, returns_result = true } +# accept = { method_id = 3, returns_result = true } + +[libraries."libnyash_net_plugin.so".SocketClientBox] +type_id = 32 + +[libraries."libnyash_net_plugin.so".SocketClientBox.methods] +birth = { method_id = 0 } +connect = { method_id = 1, args = ["host", "port"], returns_result = true } +fini = { method_id = 4294967295 } + +# Optional: ResultBox normalization (recommendation) +# connect = { method_id = 1, args = ["host", "port"], returns_result = true } + +[libraries."libnyash_net_plugin.so".SocketConnBox] +type_id = 31 + +[libraries."libnyash_net_plugin.so".SocketConnBox.methods] +birth = { method_id = 0 } +send = { method_id = 1 } +recv = { method_id = 2 } +close = { method_id = 3 } +recvTimeout = { method_id = 4, args = ["timeout_ms"], returns_result = true } +fini = { method_id = 4294967295 } diff --git a/plugins/nyash-net-plugin/src/lib.rs b/plugins/nyash-net-plugin/src/lib.rs index c80bc07f..92100ea5 100644 --- a/plugins/nyash-net-plugin/src/lib.rs +++ b/plugins/nyash-net-plugin/src/lib.rs @@ -95,6 +95,7 @@ const M_CONN_RECV_TIMEOUT: u32 = 4; // ms -> bytes (empty if timeout) static SERVER_INSTANCES: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); static SERVER_START_SEQ: AtomicU32 = AtomicU32::new(1); static ACTIVE_SERVER_ID: Lazy>> = Lazy::new(|| Mutex::new(None)); +static LAST_ACCEPTED_REQ: Lazy>> = Lazy::new(|| Mutex::new(None)); static REQUESTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); static RESPONSES: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); static CLIENTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); @@ -121,8 +122,7 @@ struct RequestState { response_id: Option, // For HTTP-over-TCP server: map to an active accepted socket to respond on server_conn_id: Option, - // Which logical HttpServer instance this request belongs to - server_id: Option, + responded: bool, } struct ResponseState { @@ -132,8 +132,6 @@ struct ResponseState { // For HTTP-over-TCP client: associated socket connection id to read from client_conn_id: Option, parsed: bool, - // Which server this response is expected from (by server instance id) - server_id: Option, } struct ClientState; @@ -231,7 +229,7 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) }); let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: resp_hint, server_conn_id: Some(conn_id), server_id: Some(server_id_copy) }); + REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: resp_hint, server_conn_id: Some(conn_id), responded: false }); if let Some(h) = resp_hint { netlog!("http:accept linked resp_id hint={} for req_id={} conn_id={}", h, req_id, conn_id); } pending.lock().unwrap().push_back(req_id); } else { @@ -281,6 +279,7 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: } else { None } } { netlog!("server.accept: return req_id={} srv_id={}", req_id, id); + *LAST_ACCEPTED_REQ.lock().unwrap() = Some(req_id); return write_tlv_handle(T_REQUEST, req_id, res, res_len); } std::thread::sleep(Duration::from_millis(5)); @@ -295,7 +294,7 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re match m { M_BIRTH => { let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - REQUESTS.lock().unwrap().insert(id, RequestState { path: String::new(), body: vec![], response_id: None, server_conn_id: None, server_id: None }); + REQUESTS.lock().unwrap().insert(id, RequestState { path: String::new(), body: vec![], response_id: None, server_conn_id: None, responded: false }); write_u32(id, res, res_len) } M_REQ_PATH => { @@ -363,29 +362,42 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re rq_map2.get(&id).and_then(|rq2| rq2.response_id) } { let mut resp_map = RESPONSES.lock().unwrap(); - let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true, server_id: None }); + let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true }); dst.status = status; dst.headers = headers.clone(); dst.body = body.clone(); netlog!("Request.respond: mirrored client handle id={} body_len={} headers={} status={}", target_id, dst.body.len(), dst.headers.len(), dst.status); } + // mark responded + { + let mut rq_map3 = REQUESTS.lock().unwrap(); + if let Some(rq3) = rq_map3.get_mut(&id) { rq3.responded = true; } + } return write_tlv_void(res, res_len); } - // Not backed by a socket: attempt to locate the active TCP-backed request for the same server and respond on it - let this_server = rq.server_id; - drop(rq_map); // release before taking REQUESTS again - let alt = REQUESTS.lock().unwrap().iter() - .filter_map(|(rid, rqs)| { - if rqs.server_conn_id.is_some() && rqs.server_id == this_server { Some((*rid, rqs.server_conn_id.unwrap(), rqs.response_id)) } else { None } - }) - .max_by_key(|(rid, _, _)| *rid); - if let Some((rid_alt, conn_id_alt, resp_hint_alt)) = alt { + // Not backed by a socket: attempt reroute to last accepted or latest TCP-backed unresponded request + drop(rq_map); + let candidate_req = { + if let Some(last_id) = *LAST_ACCEPTED_REQ.lock().unwrap() { + if let Some(r) = REQUESTS.lock().unwrap().get(&last_id) { + if r.server_conn_id.is_some() && !r.responded { Some(last_id) } else { None } + } else { None } + } else { None } + }.or_else(|| { + REQUESTS.lock().unwrap().iter() + .filter_map(|(rid, rqs)| if rqs.server_conn_id.is_some() && !rqs.responded { Some(*rid) } else { None }) + .max() + }); + if let Some(target_req_id) = candidate_req { + let (conn_id_alt, resp_hint_alt) = { + let map = REQUESTS.lock().unwrap(); + let r = map.get(&target_req_id).unwrap(); + (r.server_conn_id.unwrap(), r.response_id) + }; let (status, headers, body) = { let resp_map = RESPONSES.lock().unwrap(); - if let Some(src) = resp_map.get(&provided_resp_id) { - (src.status, src.headers.clone(), src.body.clone()) - } else { return E_INV_HANDLE } + if let Some(src) = resp_map.get(&provided_resp_id) { (src.status, src.headers.clone(), src.body.clone()) } else { return E_INV_HANDLE } }; let reason = match status { 200 => "OK", 201 => "Created", 204 => "No Content", 400 => "Bad Request", 404 => "Not Found", 500 => "Internal Server Error", _ => "OK" }; let mut buf = Vec::new(); @@ -393,47 +405,22 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re let mut has_len = false; for (k,v) in &headers { if k.eq_ignore_ascii_case("Content-Length") { has_len = true; } buf.extend_from_slice(format!("{}: {}\r\n", k, v).as_bytes()); } if !has_len { buf.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes()); } - buf.extend_from_slice(b"Connection: close\r\n\r\n"); - buf.extend_from_slice(&body); - netlog!("Request.respond: reroute TCP send via req_id={} conn_id={}", rid_alt, conn_id_alt); + buf.extend_from_slice(b"Connection: close\r\n\r\n"); buf.extend_from_slice(&body); + netlog!("Request.respond: reroute TCP send via req_id={} conn_id={}", target_req_id, conn_id_alt); if let Some(conn) = SOCK_CONNS.lock().unwrap().remove(&conn_id_alt) { if let Ok(mut s) = conn.stream.lock() { let _ = s.write_all(&buf); let _ = s.flush(); } } if let Some(target_id) = resp_hint_alt { let mut resp_map = RESPONSES.lock().unwrap(); - let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true, server_id: None }); + let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true }); dst.status = status; dst.headers = headers.clone(); dst.body = body.clone(); netlog!("Request.respond: mirrored client handle id={} body_len={} headers={} status={}", target_id, dst.body.len(), dst.headers.len(), dst.status); } + if let Some(rq4) = REQUESTS.lock().unwrap().get_mut(&target_req_id) { rq4.responded = true; } return write_tlv_void(res, res_len); } - - // Stub fallback: copy into paired client-side response - // If this request has no hint, try to find a concurrent TCP-backed request's hint - // Re-acquire to compute fallback target_id within same server and update stub rq.response_id - let mut target_id_opt: Option = None; - if let Some((_rid_alt, _conn_id_alt, resp_hint_alt)) = { - REQUESTS.lock().unwrap().iter() - .filter_map(|(rid, rqs)| { if rqs.server_conn_id.is_some() && rqs.server_id == this_server { Some((*rid, rqs.server_conn_id.unwrap(), rqs.response_id)) } else { None } }) - .max_by_key(|(rid,_,_)| *rid) - } { if let Some(h) = resp_hint_alt { target_id_opt = Some(h); } } - let target_id = target_id_opt.unwrap_or(provided_resp_id); - { - let mut rq_map2 = REQUESTS.lock().unwrap(); - if let Some(rq2) = rq_map2.get_mut(&id) { rq2.response_id = Some(target_id); } - } - let mut resp_map = RESPONSES.lock().unwrap(); - let (src_status, src_headers, src_body) = if let Some(src) = resp_map.get(&provided_resp_id) { - (src.status, src.headers.clone(), src.body.clone()) - } else { return E_INV_HANDLE }; - let dst = resp_map.entry(target_id).or_insert(ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: true, server_id: None }); - dst.status = src_status; - dst.headers = src_headers; - dst.body = src_body; - dst.parsed = true; - dst.client_conn_id = None; - netlog!("Request.respond: fallback copy to client handle id={} body_len={}", target_id, dst.body.len()); - return write_tlv_void(res, res_len); + netlog!("Request.respond: no suitable TCP-backed request found for reroute; invalid handle"); + return E_INV_HANDLE; } E_INV_HANDLE } @@ -445,7 +432,8 @@ unsafe fn response_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res match m { M_BIRTH => { let id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed); - RESPONSES.lock().unwrap().insert(id, ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false, server_id: None }); + RESPONSES.lock().unwrap().insert(id, ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false }); + netlog!("Response.birth: new id={}", id); write_u32(id, res, res_len) } M_RESP_SET_STATUS => { @@ -471,6 +459,7 @@ unsafe fn response_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res write_tlv_void(res, res_len) } M_RESP_READ_BODY => { + netlog!("HttpResponse.readBody: enter id={}", id); // If bound to a client connection, lazily read and parse (with short retries) for _ in 0..50 { let need_parse = { @@ -483,22 +472,10 @@ unsafe fn response_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res std::thread::sleep(Duration::from_millis(5)); } else { break; } } - // If this response is empty but another response from the same server has data, mirror it for robustness - let mut body_to_return: Option> = None; - { - let map = RESPONSES.lock().unwrap(); - if let Some(rp) = map.get(&id) { - if !rp.body.is_empty() { body_to_return = Some(rp.body.clone()); } - else if let Some(sid) = rp.server_id { - if let Some((_other_id, other)) = map.iter().filter(|(rid, r)| r.server_id == Some(sid) && !r.body.is_empty()).max_by_key(|(rid, _)| **rid) { - body_to_return = Some(other.body.clone()); - } - } - } else { return E_INV_HANDLE; } - } - let data = body_to_return.unwrap_or_default(); - netlog!("HttpResponse.readBody: id={} body_len={}", id, data.len()); - write_tlv_bytes(&data, res, res_len) + if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { + netlog!("HttpResponse.readBody: id={} body_len={}", id, rp.body.len()); + write_tlv_bytes(&rp.body, res, res_len) + } else { E_INV_HANDLE } } M_RESP_GET_STATUS => { for _ in 0..50 { @@ -566,7 +543,7 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: let servers = SERVER_INSTANCES.lock().unwrap(); servers.iter().find(|(_, s)| s.port == port).map(|(sid, _)| *sid) }; - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false, server_id: server_id_for_port }); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false }); tcp_ok = true; netlog!("client.get: url={} resp_id={} tcp_ok=true conn_id={}", url, resp_id, conn_id); } else { @@ -574,27 +551,10 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: let servers = SERVER_INSTANCES.lock().unwrap(); servers.iter().find(|(_, s)| s.port == port).map(|(sid, _)| *sid) }; - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false, server_id: server_id_for_port }); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false }); netlog!("client.get: url={} resp_id={} tcp_ok=false", url, resp_id); } - // Only enqueue stub request if TCP failed - if !tcp_ok { - let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - // Determine target server id we enqueue into - let mut target_server_id: Option = None; - { - let mut servers = SERVER_INSTANCES.lock().unwrap(); - if let Some((sid, s)) = servers.iter_mut().find(|(_, s)| s.port == port) { - s.pending.lock().unwrap().push_back(req_id); - target_server_id = Some(*sid); - } else if let Some((sid, s)) = servers.iter_mut().filter(|(_, s)| s.running.load(Ordering::SeqCst)).max_by_key(|(_, s)| s.start_seq) { - s.pending.lock().unwrap().push_back(req_id); - target_server_id = Some(*sid); - } - } - REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body: vec![], response_id: Some(resp_id), server_conn_id: None, server_id: target_server_id }); - netlog!("client.get: enqueued stub req_id={} for resp_id={} server_id={:?}", req_id, resp_id, target_server_id); - } + // No stub enqueue in TCP-only design write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } M_CLIENT_POST => { @@ -626,7 +586,7 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: let servers = SERVER_INSTANCES.lock().unwrap(); servers.iter().find(|(_, s)| s.port == port).map(|(sid, _)| *sid) }; - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false, server_id: server_id_for_port }); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false }); tcp_ok = true; netlog!("client.post: url={} resp_id={} tcp_ok=true conn_id={} body_len={}", url, resp_id, conn_id, body.len()); } else { @@ -634,26 +594,10 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: let servers = SERVER_INSTANCES.lock().unwrap(); servers.iter().find(|(_, s)| s.port == port).map(|(sid, _)| *sid) }; - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false, server_id: server_id_for_port }); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false }); netlog!("client.post: url={} resp_id={} tcp_ok=false body_len={}", url, resp_id, body.len()); } - // Enqueue stub request only if TCP failed - if !tcp_ok { - let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - let mut target_server_id: Option = None; - { - let mut servers = SERVER_INSTANCES.lock().unwrap(); - if let Some((sid, s)) = servers.iter_mut().find(|(_, s)| s.port == port) { - s.pending.lock().unwrap().push_back(req_id); - target_server_id = Some(*sid); - } else if let Some((sid, s)) = servers.iter_mut().filter(|(_, s)| s.running.load(Ordering::SeqCst)).max_by_key(|(_, s)| s.start_seq) { - s.pending.lock().unwrap().push_back(req_id); - target_server_id = Some(*sid); - } - } - REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: Some(resp_id), server_conn_id: None, server_id: target_server_id }); - netlog!("client.post: enqueued stub req_id={} for resp_id={} body_len={} server_id={:?}", req_id, resp_id, body_len, target_server_id); - } + // No stub enqueue in TCP-only design write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } _ => E_INV_METHOD, diff --git a/projects/nyash-wasm/pkg/nyash_rust_bg.wasm b/projects/nyash-wasm/pkg/nyash_rust_bg.wasm index 3a7f1cd2..2d49659b 100644 Binary files a/projects/nyash-wasm/pkg/nyash_rust_bg.wasm and b/projects/nyash-wasm/pkg/nyash_rust_bg.wasm differ diff --git a/src/backend/vm.rs b/src/backend/vm.rs index fa08b56a..c6718f9f 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -535,7 +535,7 @@ impl VM { // Prepare VMValue args: me + evaluated arguments let mut vm_args: Vec = Vec::new(); - vm_args.push(VMValue::from_nyash_box(box_nyash.clone_box())); + vm_args.push(VMValue::from_nyash_box(box_nyash.clone_or_share())); for arg_id in args { let arg_vm_value = self.get_value(*arg_id)?; vm_args.push(arg_vm_value); @@ -589,7 +589,7 @@ impl VM { let func_name = format!("{}.{}{}", class_name, method, format!("/{}", args.len())); // Prepare VMValue args: me + evaluated arguments (use original VM args for value-level fidelity) let mut vm_args: Vec = Vec::new(); - vm_args.push(VMValue::from_nyash_box(box_nyash.clone_box())); + vm_args.push(VMValue::from_nyash_box(box_nyash.clone_or_share())); for arg_id in args { let arg_vm_value = self.get_value(*arg_id)?; vm_args.push(arg_vm_value); diff --git a/src/box_trait.rs b/src/box_trait.rs index 61064ca6..8b1fbf8a 100644 --- a/src/box_trait.rs +++ b/src/box_trait.rs @@ -109,6 +109,14 @@ pub trait NyashBox: BoxCore + Debug { /// Share this box (state-preserving reference sharing) fn share_box(&self) -> Box; + /// Identity hint: boxes that wrap external/stateful handles should override to return true. + fn is_identity(&self) -> bool { false } + + /// Helper: pick share or clone based on identity semantics. + fn clone_or_share(&self) -> Box { + if self.is_identity() { self.share_box() } else { self.clone_box() } + } + /// Arc参照を返す新しいcloneメソッド(参照共有) fn clone_arc(&self) -> SharedNyashBox { Arc::from(self.clone_box()) @@ -941,4 +949,4 @@ mod tests { assert_eq!(v.type_name(), "VoidBox"); assert_eq!(v.to_string_box().value, "void"); } -} \ No newline at end of file +} diff --git a/src/boxes/result/mod.rs b/src/boxes/result/mod.rs index cfc8b9e4..0cdf7712 100644 --- a/src/boxes/result/mod.rs +++ b/src/boxes/result/mod.rs @@ -39,14 +39,17 @@ impl NyashResultBox { impl NyashBox for NyashResultBox { fn clone_box(&self) -> Box { match self { - NyashResultBox::Ok(val) => Box::new(NyashResultBox::Ok(val.clone_box())), - NyashResultBox::Err(err) => Box::new(NyashResultBox::Err(err.clone_box())), + NyashResultBox::Ok(val) => Box::new(NyashResultBox::Ok(val.clone_or_share())), + NyashResultBox::Err(err) => Box::new(NyashResultBox::Err(err.clone_or_share())), } } /// 仮実装: clone_boxと同じ(後で修正) fn share_box(&self) -> Box { - self.clone_box() + match self { + NyashResultBox::Ok(val) => Box::new(NyashResultBox::Ok(val.share_box())), + NyashResultBox::Err(err) => Box::new(NyashResultBox::Err(err.share_box())), + } } fn to_string_box(&self) -> StringBox { @@ -126,7 +129,14 @@ impl ResultBox { /// getValue()の実装 - Ok値を取得 pub fn get_value(&self) -> Box { match self { - NyashResultBox::Ok(val) => val.clone_box(), + NyashResultBox::Ok(val) => { + // Preserve identity for plugin-backed boxes + if val.as_any().downcast_ref::().is_some() { + val.share_box() + } else { + val.clone_box() + } + } NyashResultBox::Err(_) => Box::new(StringBox::new("Error: Result is Err")), } } diff --git a/src/environment.rs b/src/environment.rs index 6d5ac029..3fbe612c 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -81,7 +81,7 @@ impl Environment { pub fn get(&self, name: &str) -> Result, EnvironmentError> { // 現在のスコープから検索 if let Some(value) = self.bindings.lock().unwrap().get(name) { - return Ok(value.clone_box()); + return Ok(value.clone_or_share()); } // 親スコープから検索 @@ -107,7 +107,7 @@ impl Environment { // 親スコープで再帰的に検索・設定 if let Some(parent) = &self.parent { - match parent.lock().unwrap().set(&name, value.clone_box()) { + match parent.lock().unwrap().set(&name, value.clone_or_share()) { Ok(()) => return Ok(()), Err(EnvironmentError::UndefinedVariable { .. }) => { // 親にもない場合は現在のスコープに新規定義 @@ -356,4 +356,4 @@ mod tests { _ => panic!("Expected UndefinedVariable error"), } } -} \ No newline at end of file +} diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index e3b98e11..a0d945b5 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -882,7 +882,7 @@ impl NyashInterpreter { }; // 🌍 this変数をバインドしてstatic初期化実行(me構文のため) - self.declare_local_variable("me", (*static_instance).clone_box()); + self.declare_local_variable("me", (*static_instance).clone_or_share()); for stmt in init_statements { self.execute_statement(stmt)?; diff --git a/src/interpreter/delegation.rs b/src/interpreter/delegation.rs index d358163b..5632ac1a 100644 --- a/src/interpreter/delegation.rs +++ b/src/interpreter/delegation.rs @@ -126,11 +126,11 @@ impl NyashInterpreter { self.local_vars.clear(); // 'me'を現在のインスタンスに設定(重要:現在のインスタンスを維持) - self.declare_local_variable("me", current_instance_val.clone_box()); + self.declare_local_variable("me", current_instance_val.clone_or_share()); // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // 親メソッドの本体を実行 @@ -199,11 +199,11 @@ impl NyashInterpreter { self.local_vars.clear(); // 'me'を現在のインスタンスに設定 - self.declare_local_variable("me", current_instance.clone_box()); + self.declare_local_variable("me", current_instance.clone_or_share()); // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // 親コンストラクタの本体を実行 @@ -356,4 +356,4 @@ impl NyashInterpreter { } } } -} \ No newline at end of file +} diff --git a/src/interpreter/expressions/access.rs b/src/interpreter/expressions/access.rs index 50920a03..e992e379 100644 --- a/src/interpreter/expressions/access.rs +++ b/src/interpreter/expressions/access.rs @@ -89,7 +89,7 @@ impl NyashInterpreter { // Convert back to Box for now if let Ok(box_value) = weak_value.to_box() { if let Ok(inner_box) = box_value.try_lock() { - return Ok(Arc::from(inner_box.clone_box())); + return Ok(Arc::from(inner_box.clone_or_share())); } } } @@ -149,7 +149,7 @@ impl NyashInterpreter { })?; // Convert Arc to Box for compatibility - Ok((*shared_field).clone_box()) + Ok((*shared_field).clone_or_share()) } diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index 3ca7e5c1..051ed7d0 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -65,7 +65,7 @@ impl NyashInterpreter { // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // static関数の本体を実行 @@ -194,7 +194,7 @@ impl NyashInterpreter { // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // メソッドの本体を実行 @@ -542,7 +542,7 @@ impl NyashInterpreter { self.local_vars.clear(); // thisをlocal変数として設定 - self.declare_local_variable("me", obj_value.clone_box()); + self.declare_local_variable("me", obj_value.clone_or_share()); // fini()メソッドの本体を実行 let mut _result = Box::new(VoidBox::new()) as Box; @@ -600,11 +600,11 @@ impl NyashInterpreter { self.local_vars.clear(); // thisをlocal変数として設定 - self.declare_local_variable("me", obj_value.clone_box()); + self.declare_local_variable("me", obj_value.clone_or_share()); // パラメータをlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // メソッド本体を実行 @@ -880,11 +880,11 @@ impl NyashInterpreter { self.local_vars.clear(); // 'me'を現在のインスタンスに設定(重要:現在のインスタンスを維持) - self.declare_local_variable("me", current_instance_val.clone_box()); + self.declare_local_variable("me", current_instance_val.clone_or_share()); // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // 親メソッドの本体を実行 @@ -956,11 +956,11 @@ impl NyashInterpreter { self.local_vars.clear(); // 'me'を現在のインスタンスに設定 - self.declare_local_variable("me", current_instance.clone_box()); + self.declare_local_variable("me", current_instance.clone_or_share()); // 引数をlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // 親コンストラクタの本体を実行 diff --git a/src/interpreter/expressions/mod.rs b/src/interpreter/expressions/mod.rs index 443baa2b..33cfe555 100644 --- a/src/interpreter/expressions/mod.rs +++ b/src/interpreter/expressions/mod.rs @@ -55,7 +55,7 @@ impl NyashInterpreter { ASTNode::FieldAccess { object, field, .. } => { let shared_result = self.execute_field_access(object, field)?; - Ok((*shared_result).clone_box()) // Convert Arc to Box for external interface + Ok((*shared_result).clone_or_share()) } ASTNode::New { class, arguments, type_arguments, .. } => { @@ -68,7 +68,7 @@ impl NyashInterpreter { .map_err(|_| RuntimeError::InvalidOperation { message: "'this' is only available inside methods".to_string(), })?; - Ok((*shared_this).clone_box()) // Convert for external interface + Ok((*shared_this).clone_or_share()) } ASTNode::Me { .. } => { @@ -79,7 +79,7 @@ impl NyashInterpreter { message: "'me' is only available inside methods".to_string(), })?; - Ok((*shared_me).clone_box()) // Convert for external interface + Ok((*shared_me).clone_or_share()) } ASTNode::ThisField { field, .. } => { @@ -94,7 +94,7 @@ impl NyashInterpreter { .ok_or_else(|| RuntimeError::InvalidOperation { message: format!("Field '{}' not found on this", field) })?; - Ok((*shared_field).clone_box()) // Convert for external interface + Ok((*shared_field).clone_or_share()) } else { Err(RuntimeError::TypeError { message: "'this' is not an instance".to_string(), @@ -114,7 +114,7 @@ impl NyashInterpreter { .ok_or_else(|| RuntimeError::InvalidOperation { message: format!("Field '{}' not found on me", field) })?; - Ok((*shared_field).clone_box()) // Convert for external interface + Ok((*shared_field).clone_or_share()) } else { Err(RuntimeError::TypeError { message: "'this' is not an instance".to_string(), @@ -202,4 +202,4 @@ impl NyashInterpreter { // } -} \ No newline at end of file +} diff --git a/src/interpreter/field_access.rs b/src/interpreter/field_access.rs index 3613ca5e..bf53ed26 100644 --- a/src/interpreter/field_access.rs +++ b/src/interpreter/field_access.rs @@ -48,7 +48,7 @@ impl NyashInterpreter { message: format!("Field '{}' not found in static box '{}'", field, box_name), })?; - Ok((*field_value).clone_box()) + Ok((*field_value).clone_or_share()) } else { Err(RuntimeError::InvalidOperation { message: format!("Static box '{}' not found", box_name), @@ -125,4 +125,4 @@ impl NyashInterpreter { let static_boxes = self.shared.static_boxes.read().unwrap(); static_boxes.contains_key(name) } -} \ No newline at end of file +} diff --git a/src/interpreter/functions.rs b/src/interpreter/functions.rs index e4e8bba4..7add8998 100644 --- a/src/interpreter/functions.rs +++ b/src/interpreter/functions.rs @@ -51,7 +51,7 @@ impl NyashInterpreter { // パラメータをlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // 関数本体を実行 @@ -94,4 +94,4 @@ impl NyashInterpreter { eprintln!("Warning: Failed to register global function: {}", err); }); } -} \ No newline at end of file +} diff --git a/src/interpreter/methods/io_methods.rs b/src/interpreter/methods/io_methods.rs index 2107e070..3a1ed4cd 100644 --- a/src/interpreter/methods/io_methods.rs +++ b/src/interpreter/methods/io_methods.rs @@ -8,7 +8,8 @@ */ use super::super::*; -use crate::box_trait::{ResultBox, StringBox, NyashBox}; +use crate::boxes::ResultBox; +use crate::box_trait::{StringBox, NyashBox}; use crate::boxes::FileBox; // use crate::bid::plugin_box::PluginFileBox; // legacy - FileBox専用 @@ -77,7 +78,7 @@ impl NyashInterpreter { pub(in crate::interpreter) fn execute_result_method(&mut self, result_box: &ResultBox, method: &str, arguments: &[ASTNode]) -> Result, RuntimeError> { match method { - "isOk" => { + "isOk" | "is_ok" => { if !arguments.is_empty() { return Err(RuntimeError::InvalidOperation { message: format!("isOk() expects 0 arguments, got {}", arguments.len()), @@ -85,7 +86,7 @@ impl NyashInterpreter { } Ok(result_box.is_ok()) } - "getValue" => { + "getValue" | "get_value" => { if !arguments.is_empty() { return Err(RuntimeError::InvalidOperation { message: format!("getValue() expects 0 arguments, got {}", arguments.len()), @@ -93,7 +94,7 @@ impl NyashInterpreter { } Ok(result_box.get_value()) } - "getError" => { + "getError" | "get_error" => { if !arguments.is_empty() { return Err(RuntimeError::InvalidOperation { message: format!("getError() expects 0 arguments, got {}", arguments.len()), @@ -343,4 +344,4 @@ impl NyashInterpreter { self.execute_plugin_method_generic(plugin_file_box, method, arguments) } */ -} \ No newline at end of file +} diff --git a/src/interpreter/objects.rs b/src/interpreter/objects.rs index 68c9b4f0..0cea036d 100644 --- a/src/interpreter/objects.rs +++ b/src/interpreter/objects.rs @@ -941,11 +941,11 @@ impl NyashInterpreter { // パラメータをlocal変数として設定 for (param, value) in params.iter().zip(arg_values.iter()) { - self.declare_local_variable(param, value.clone_box()); + self.declare_local_variable(param, value.clone_or_share()); } // this(me)をlocal変数として設定 - self.declare_local_variable("me", instance.clone_box()); + self.declare_local_variable("me", instance.clone_or_share()); // コンストラクタコンテキストを設定 let old_context = self.current_constructor_context.clone(); diff --git a/src/interpreter/special_methods.rs b/src/interpreter/special_methods.rs index 15508509..f0bfbac3 100644 --- a/src/interpreter/special_methods.rs +++ b/src/interpreter/special_methods.rs @@ -183,11 +183,11 @@ impl NyashInterpreter { self.local_vars.clear(); // meをlocal変数として設定(インスタンス自体) - self.declare_local_variable("me", instance.clone_box()); + self.declare_local_variable("me", instance.clone_or_share()); // パラメータをlocal変数として設定 for (param, arg) in params.iter().zip(args.iter()) { - self.declare_local_variable(param, arg.clone_box()); + self.declare_local_variable(param, arg.clone_or_share()); } // メソッド本体を実行 @@ -218,4 +218,4 @@ impl NyashInterpreter { }) } } -} \ No newline at end of file +} diff --git a/src/interpreter/statements.rs b/src/interpreter/statements.rs index 4c02739d..40daa67e 100644 --- a/src/interpreter/statements.rs +++ b/src/interpreter/statements.rs @@ -158,7 +158,7 @@ impl NyashInterpreter { ASTNode::GlobalVar { name, value, .. } => { let val = self.execute_expression(value)?; // 🌍 革命的グローバル変数:GlobalBoxのフィールドとして設定 - self.set_variable(name, val.clone_box())?; + self.set_variable(name, val.clone_or_share())?; Ok(Box::new(VoidBox::new())) } diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 6627ac8d..42c6990b 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -121,6 +121,7 @@ mod enabled { } impl NyashBox for PluginBoxV2 { + fn is_identity(&self) -> bool { true } fn type_name(&self) -> &'static str { // Return the actual box type name for proper method dispatch match self.box_type.as_str() { @@ -131,12 +132,11 @@ mod enabled { fn clone_box(&self) -> Box { eprintln!("🔍 DEBUG: PluginBoxV2::clone_box called for {} (id={})", self.box_type, self.inner.instance_id); - - // Clone means creating a new instance by calling birth() + // 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(); - let tlv_args = vec![1u8, 0, 0, 0]; // version=1, argc=0 - + let tlv_args = [1u8, 0, 0, 0]; // version=1, argc=0 + let result = unsafe { (self.inner.invoke_fn)( self.inner.type_id, @@ -148,17 +148,12 @@ mod enabled { &mut output_len, ) }; - + if result == 0 && output_len >= 4 { - // Extract new instance_id from output let new_instance_id = u32::from_le_bytes([ - output_buffer[0], output_buffer[1], - output_buffer[2], output_buffer[3] + 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); - - // Return new PluginBoxV2 with new instance_id (separate inner handle) Box::new(PluginBoxV2 { box_type: self.box_type.clone(), inner: std::sync::Arc::new(PluginHandleInner { @@ -171,7 +166,6 @@ mod enabled { }) } else { eprintln!("❌ clone_box failed: birth() returned error code {}", result); - // Fallback: return error message as StringBox Box::new(StringBox::new(format!("Clone failed for {}", self.box_type))) } } @@ -485,6 +479,7 @@ impl PluginBoxV2 { } buf }; + 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 { @@ -536,6 +531,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); // 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 @@ -568,14 +564,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 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 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 returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box) } else { None } }, _ => None, @@ -623,10 +622,13 @@ impl PluginBoxV2 { // Call init if available if let Some(init) = init_fn { let result = unsafe { init() }; + eprintln!("[PluginLoaderV2] nyash_plugin_init rc={} for {}", result, lib_name); if result != 0 { eprintln!("Plugin init failed with code: {}", result); return Err(BidError::PluginError); } + } else { + eprintln!("[PluginLoaderV2] nyash_plugin_init not found for {} (optional)", lib_name); } // Store plugin with Arc-wrapped library diff --git a/test_output.txt b/test_output.txt index 161c3274..dac0ea08 100644 --- a/test_output.txt +++ b/test_output.txt @@ -1,2 +1,197 @@ -Hello from Nyash! -FileBox is working! 🎉 +🔍 DEBUG: Initializing v2 plugin system +[net] Net plugin initialized, LOG_ON=true, LOG_PATH=net_plugin.log +Net plugin: LOG_ON=true, LOG_PATH=net_plugin.log +[PluginLoaderV2] nyash_plugin_init rc=0 for libnyash_net_plugin.so +[PluginLoaderV2] nyash_plugin_init rc=0 for libnyash_counter_plugin.so +[FileBox] Plugin initialized +[PluginLoaderV2] nyash_plugin_init rc=0 for libnyash_filebox_plugin.so +🔌 v2 plugin system initialized from nyash.toml + 📦 Registering plugin provider for HttpServerBox + 📦 Registering plugin provider for HttpClientBox + 📦 Registering plugin provider for HttpResponseBox + 📦 Registering plugin provider for HttpRequestBox + 📦 Registering plugin provider for SocketServerBox + 📦 Registering plugin provider for SocketClientBox + 📦 Registering plugin provider for SocketConnBox + 📦 Registering plugin provider for CounterBox + 📦 Registering plugin provider for FileBox +✅ v2 plugin system fully configured +🦀 Nyash Rust Implementation - Executing file: local_tests/test_http_simple_e2e.nyash 🦀 +🔥 Debug fuel limit: 100000 iterations +==================================================== +📝 File contents: +// Very simple HTTP test +local srv, cli, r, resp +srv = new HttpServerBox() +print("HttpServerBox created") + +// Start server +local startResult = srv.start(8090) +print("Server start result:") +print(startResult) + +// Create client +cli = new HttpClientBox() +print("HttpClientBox created") + +// Make request +r = cli.get("http://localhost:8090/test") +print("Request made") + +// Accept connection +local acceptResult = srv.accept() +print("Accept result:") +print(acceptResult) + +// Get value from accept result +local req = acceptResult.get_value() +print("Got request value") + +// Create response +resp = new HttpResponseBox() +resp.write("Hello!") +print("Response created and written") + +// Send response +req.respond(resp) +print("Response sent") + +// Get response on client side +local clientResp = r.get_value() +print("Got client response") + +// Read body +local body = clientResp.readBody() +print("Body: " + body) + +🚀 Parsing and executing... + +🔍 DEBUG: Starting parse with fuel: Some(100000)... +🔍 DEBUG: Parse completed, AST created +🔍 DEBUG: About to print parse success message... +✅ Parse successful! +🔍 DEBUG: Parse success message printed +🔍 DEBUG: Creating interpreter... +🔍 DEBUG: Starting execution... +🔍 create_box called for: HttpServerBox +🔍 Config loaded successfully +🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 nyash.toml read successfully +🔍 nyash.toml parsed successfully +🔍 Found box config for HttpServerBox with type_id: 20 +🔍 Preparing to call birth() with type_id: 20 +🔍 Output buffer allocated, about to call plugin invoke_fn... +🔍 Calling invoke_fn(type_id=20, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024) +🔍 invoke_fn returned with result: 0 +🎉 birth() success: HttpServerBox instance_id=1 +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=1) +HttpServerBox created +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=1) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: start +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpServerBox.start +[PluginLoaderV2] Invoke HttpServerBox.start: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: Integer(8090) -> I32(tag=2) +[net] http:listener bound 127.0.0.1:8090 +Server start result: +Ok(void) +🔍 create_box called for: HttpClientBox +🔍 Config loaded successfully +🔍 Found library: libnyash_net_plugin.so for box type: HttpClientBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 nyash.toml read successfully +🔍 nyash.toml parsed successfully +🔍 Found box config for HttpClientBox with type_id: 23 +🔍 Preparing to call birth() with type_id: 23 +🔍 Output buffer allocated, about to call plugin invoke_fn... +🔍 Calling invoke_fn(type_id=23, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024) +🔍 invoke_fn returned with result: 0 +🎉 birth() success: HttpClientBox instance_id=1 +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=1) +HttpClientBox created +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=1) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: get +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpClientBox.get +[PluginLoaderV2] Invoke HttpClientBox.get: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: String(len=26) -> String(tag=6) +[net] client.get: url=http://localhost:8090/test resp_id=1 tcp_ok=true conn_id=1 +[net] http:accept linked resp_id hint=1 for req_id=1 conn_id=2 +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=1) +Request made +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=1) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: accept +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpServerBox.accept +[PluginLoaderV2] Invoke HttpServerBox.accept: resolving and encoding args (argc=0) +[net] server.accept: return req_id=1 srv_id=1 +Accept result: +🔍 DEBUG: PluginBoxV2::share_box called for HttpRequestBox (id=1) +Ok(HttpRequestBox(1)) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpRequestBox (id=1) +🔍 DEBUG: execute_method_call - object type: NyashResultBox, method: get_value +🔍 DEBUG: Checking StringBox downcast for type: NyashResultBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: PluginBoxV2::share_box called for HttpRequestBox (id=1) +🔍 DEBUG: PluginBoxV2::share_box called for HttpRequestBox (id=1) +Got request value +🔍 create_box called for: HttpResponseBox +🔍 Config loaded successfully +🔍 Found library: libnyash_net_plugin.so for box type: HttpResponseBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 nyash.toml read successfully +🔍 nyash.toml parsed successfully +🔍 Found box config for HttpResponseBox with type_id: 22 +🔍 Preparing to call birth() with type_id: 22 +🔍 Output buffer allocated, about to call plugin invoke_fn... +🔍 Calling invoke_fn(type_id=22, method_id=0, instance_id=0, tlv_args=[1, 0, 0, 0], output_buf, output_size=1024) +🔍 invoke_fn returned with result: 0 +🎉 birth() success: HttpResponseBox instance_id=2 +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=2) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=2) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: write +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpResponseBox.write +[PluginLoaderV2] Invoke HttpResponseBox.write: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: String(len=6) -> String(tag=6) +[net] HttpResponse.write: id=2 bytes_len=6 +[net] HttpResponse.write: body now has 6 bytes +Response created and written +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpRequestBox (id=1) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: respond +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpRequestBox.respond +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=2) +[PluginLoaderV2] Invoke HttpRequestBox.respond: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: PluginBoxV2(HttpResponseBox, id=2) -> Handle(tag=8) +Response sent +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=1) +🔍 DEBUG: execute_method_call - object type: NyashResultBox, method: get_value +🔍 DEBUG: Checking StringBox downcast for type: NyashResultBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=1) +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=1) +Got client response +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=1) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: readBody +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpResponseBox.readBody +[PluginLoaderV2] Invoke HttpResponseBox.readBody: resolving and encoding args (argc=0) diff --git a/tests/e2e_plugin_socket.rs b/tests/e2e_plugin_socket.rs index 37cb14fb..4e58c88c 100644 --- a/tests/e2e_plugin_socket.rs +++ b/tests/e2e_plugin_socket.rs @@ -29,9 +29,9 @@ ss = new SocketServerBox() ss.start(9100) sc = new SocketClientBox() -c = sc.connect("127.0.0.1", 9100) +c = sc.connect("127.0.0.1", 9100).get_value() -s = ss.accept() +s = ss.accept().get_value() c.send("ping") r = s.recv() @@ -56,19 +56,20 @@ local ss, sc, c, s, r ss = new SocketServerBox() ss.start(9101) -// before any client, acceptTimeout returns void +// before any client, acceptTimeout returns Err (timeout) r = ss.acceptTimeout(50) // now connect sc = new SocketClientBox() -c = sc.connect("127.0.0.1", 9101) -s = ss.acceptTimeout(500) +c = sc.connect("127.0.0.1", 9101).get_value() +s = ss.acceptTimeout(500).get_value() -// recvTimeout with no data should be empty +// recvTimeout with no data should be Err (timeout) r = s.recvTimeout(50) // send then recvTimeout should get data c.send("hello") r = s.recvTimeout(200) +r = r.get_value() r "#; @@ -77,4 +78,3 @@ r let result = interpreter.execute(ast).expect("exec failed"); assert_eq!(result.to_string_box().value, "hello"); } -