diff --git a/CLAUDE.md b/CLAUDE.md index 6119aefa..da1d7894 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,9 +3,11 @@ Nyashプログラミング言語開発に必要な情報をまとめたクイックリファレンス。 ## 🧭 Start Here (最初に見る) -- **🎯 主軸タスク**: [docs/予定/native-plan/copilot_issues.txt](docs/予定/native-plan/copilot_issues.txt) **← 最重要!** -- 現在のタスク: [docs/CURRENT_TASK.md](docs/CURRENT_TASK.md) -- コア概念(速習): [docs/nyash_core_concepts.md](docs/nyash_core_concepts.md) +- **🎯 主軸タスク**: [docs/development/roadmap/native-plan/copilot_issues.txt](docs/development/roadmap/native-plan/copilot_issues.txt) **← 最重要!** +- 現在のタスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md) +- ドキュメント入口: [docs/README.md](docs/README.md) +- コア概念(速習): [docs/reference/architecture/nyash_core_concepts.md](docs/reference/architecture/nyash_core_concepts.md) +- Netプラグイン(HTTP/TCP): [docs/reference/plugin-system/net-plugin.md](docs/reference/plugin-system/net-plugin.md) ## 🤖 **Claude×Copilot協調開発の主軸** ### 📋 **copilot_issues.txt - 開発の軸となるファイル** @@ -22,7 +24,7 @@ Nyashプログラミング言語開発に必要な情報をまとめたクイッ ## 🚀 クイックスタート ### 🎯 実行方式選択 (重要!) -- **実行バックエンド完全ガイド**: [docs/execution-backends.md](docs/execution-backends.md) +- **実行バックエンド完全ガイド**: [docs/reference/architecture/execution-backends.md](docs/reference/architecture/execution-backends.md) - インタープリター(開発・デバッグ)/ VM(高速実行)/ WASM(Web配布) - ⚡ **ベンチマーク機能**: `--benchmark` で3バックエンド性能比較(13.5倍実行高速化実証済み!) @@ -76,33 +78,34 @@ python3 -m http.server 8010 ## 📚 ドキュメント構造 ### 🎯 **最重要ドキュメント(開発者向け)** -- **[copilot_issues.txt](docs/予定/native-plan/copilot_issues.txt)** - **Phase順開発計画の軸** -- **[CURRENT_TASK.md](docs/CURRENT_TASK.md)** - 現在進行状況詳細 -- **[native-plan/README.md](docs/予定/native-plan/README.md)** - ネイティブビルド計画 +- **[copilot_issues.txt](docs/development/roadmap/native-plan/copilot_issues.txt)** - **Phase順開発計画の軸** +- **[CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md)** - 現在進行状況詳細 +- **[native-plan/README.md](docs/development/roadmap/native-plan/README.md)** - ネイティブビルド計画 ### 📖 利用者向けドキュメント -- 説明書: docs/説明書/README.md - - ネイティブビルド: docs/説明書/native-build/README.md - - WASM: docs/説明書/wasm/ - - リファレンス: docs/説明書/reference/ -- 予定: docs/予定/README.md - - CURRENT_TASK, ネイティブ計画, フェーズ課題 -- archive: docs/archive/ +- 入口: [docs/README.md](docs/README.md) + - Getting Started: [docs/guides/getting-started.md](docs/guides/getting-started.md) + - Language Guide: [docs/guides/language-guide.md](docs/guides/language-guide.md) + - Reference: [docs/reference/](docs/reference/) +- 開発計画/進捗: [docs/development/](docs/development/) + - 現在タスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md) + - ネイティブ計画: [docs/development/roadmap/native-plan/](docs/development/roadmap/native-plan/) + - フェーズ課題: [docs/development/roadmap/](docs/development/roadmap/) +- アーカイブ: [docs/archive/](docs/archive/) ### 🎯 よく使う情報 -- Getting Started: docs/説明書/GETTING_STARTED.md -- Language Guide: docs/説明書/LANGUAGE_GUIDE.md -- Playground Guide: docs/説明書/guides/playground_guide.md +- Getting Started: [docs/guides/getting-started.md](docs/guides/getting-started.md) +- Language Guide: [docs/guides/language-guide.md](docs/guides/language-guide.md) +- Playground Guide: [docs/guides/tutorials/playground_guide.md](docs/guides/tutorials/playground_guide.md) ### 📊 最新開発状況 -- 現在のタスク: docs/CURRENT_TASK.md -- 予定インデックス: docs/予定/README.md +- 現在のタスク: [docs/development/current/CURRENT_TASK.md](docs/development/current/CURRENT_TASK.md) +- ロードマップ: [docs/development/roadmap/](docs/development/roadmap/) ### 📖 詳細リファレンス -- リファレンス: docs/説明書/reference/ - - 言語: docs/説明書/reference/language-reference.md - - デリゲーション構文: docs/説明書/reference/override-delegation-syntax.md - - ビルトイン: docs/説明書/reference/builtin-boxes.md - - fini/弱参照: docs/説明書/reference/finalization-system.md +- リファレンス: [docs/reference/](docs/reference/) + - 言語仕様: [docs/reference/language/LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md) + - 可視性/デリゲーション: [docs/reference/language/field-visibility-and-delegation.md](docs/reference/language/field-visibility-and-delegation.md) + - Box/プラグイン: [docs/reference/boxes-system/](docs/reference/boxes-system/), [docs/reference/plugin-system/](docs/reference/plugin-system/) ### 🎮 実用例・アプリ -- **[実用例](docs/説明書/guides/)** - サンプルコード・パターン集 +- **[実用例](docs/guides/)** - サンプルコード・パターン集 - **実装済みアプリ**: サイコロRPG・統計計算・LISPインタープリター ## ⚡ 重要な設計原則 @@ -285,12 +288,12 @@ app.setSize(800, 600) #### 🔤 言語仕様 - **[構文早見表](docs/quick-reference/syntax-cheatsheet.md)** - 基本構文・よくある間違い -- **[完全リファレンス](docs/説明書/reference/)** - 言語仕様詳細 -- **[予約語一覧](docs/説明書/reference/keywords.md)** - from, override, pack等 +- **[完全リファレンス](docs/reference/)** - 言語仕様詳細 +- 予約語や構文: [docs/reference/language/LANGUAGE_REFERENCE_2025.md](docs/reference/language/LANGUAGE_REFERENCE_2025.md) #### 📦 主要BOXのAPI -- **[ビルトイン型](docs/説明書/reference/built-in-boxes.md)** - 全Box型API仕様 -- **P2PBox & IntentBox** - docs/説明書/reference/built-in-boxes.md内 +- **[Box/プラグイン関連](docs/reference/boxes-system/)** - APIと設計 +- **P2PBox & IntentBox** - `docs/reference/boxes-system/` を参照 - **StringBox, IntegerBox, ConsoleBox** - 基本Box API - **EguiBox, DebugBox, MathBox** - 特殊Box API @@ -300,7 +303,7 @@ app.setSize(800, 600) Read src/boxes/p2p_box.rs # 直接ソース参照 # ✅ 良い例:ドキュメント優先 -Read docs/説明書/reference/built-in-boxes.md # まずドキュメント +Read docs/reference/ # まずドキュメント(API/言語仕様の入口) # → 古い/不足 → ドキュメント更新 # → それでも不明 → ソース確認 ``` diff --git a/docs/README.md b/docs/README.md index 661ab1bc..5fa26351 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,9 @@ - native-plan/ - ネイティブビルド計画 - **proposals/** - RFC、新機能提案 +### 🔌 Net Plugin(HTTP/TCP) +- 使い方と仕様: `reference/plugin-system/net-plugin.md` + ### 🗄️ [archive/](archive/) - アーカイブ - **consultations/** - AI相談記録(gemini/chatgpt/codex) - **decisions/** - 過去の設計決定 diff --git a/docs/reference/core-language/language_spec.md b/docs/reference/core-language/language_spec.md index 3687a359..a38caa7f 100644 --- a/docs/reference/core-language/language_spec.md +++ b/docs/reference/core-language/language_spec.md @@ -97,6 +97,28 @@ box ClassName { } ``` +#### フィールドの可視性(public/private) +- `init { ... }` で宣言したフィールドは既定で public(外部から参照・代入可能) +- 追加で private フィールドを宣言する場合は `private { ... }` ブロックを使用 +- 外部から private フィールドへ直接アクセスすることはできません(ゲッター/セッター経由) + +```nyash +box User { + // 既定: init は public フィールド宣言 + init { name } + + // private フィールド宣言 + private { age, passwordHash } + + birth(n, a) { me.name = n; me.age = a } + + setAge(a) { me.age = a } + getAge() { return me.age } +} +``` + +補足: 旧仕様では `init` の可視性が明確ではありませんでしたが、現在は「public」に統一されています。 + #### **デリゲーションBox** ```nyash box Child from Parent interface Comparable { @@ -522,4 +544,4 @@ loop(condition) { } # 統一ループ構文 **🎉 Nyash 2025は、AI協働設計による最先端言語システムとして、シンプルさと強力さを完全に両立しました。** -*最終更新: 2025年8月10日 - Arc Revolution + AI大相談会成功記念* \ No newline at end of file +*最終更新: 2025年8月10日 - Arc Revolution + AI大相談会成功記念* diff --git a/docs/reference/plugin-system/README.md b/docs/reference/plugin-system/README.md index fd6e5ffd..b6a364de 100644 --- a/docs/reference/plugin-system/README.md +++ b/docs/reference/plugin-system/README.md @@ -21,6 +21,25 @@ - プラグインの動作確認とデバッグに使用 - `tools/plugin-tester`ツールの使用方法 +- **[plugin_lifecycle.md](./plugin_lifecycle.md)** - ライフサイクル/RAII/シングルトン/ログ + - 共有ハンドル、scope終了時の扱い、`shutdown_plugins_v2()` の動作 + - NetPlugin(HTTP/TCP)の並列E2E時の注意点 + +- **[net-plugin.md](./net-plugin.md)** - Netプラグイン(HTTP/TCP PoC) + - GET/POST、ヘッダ、Content-Length、環境変数によるログ + +### ⚙️ 戻り値のResult化(B案サポート) +- `nyash.toml` のメソッド定義に `returns_result = true` を付けると、 + - 成功: `Ok(value)` の `ResultBox` に包んで返す + - 失敗(BID負エラー): `Err(ErrorBox(message))` を返す(例外にはしない) + +```toml +[libraries."libnyash_example.so".ExampleBox.methods] +dangerousOp = { method_id = 10, returns_result = true } +``` + +未指定の場合は従来通り(成功=生値、失敗=例外として伝播)。 + - **[filebox-bid-mapping.md](./filebox-bid-mapping.md)** - 参考資料 - FileBox APIとプラグイン実装の対応表 - API設計の参考として有用 @@ -84,4 +103,4 @@ cargo build --release --- **Status**: Phase 2 Documentation Reorganization - Completed -**Last Updated**: 2025-08-20 \ No newline at end of file +**Last Updated**: 2025-08-20 diff --git a/docs/reference/plugin-system/net-plugin.md b/docs/reference/plugin-system/net-plugin.md new file mode 100644 index 00000000..04e0221b --- /dev/null +++ b/docs/reference/plugin-system/net-plugin.md @@ -0,0 +1,72 @@ +# Net Plugin (HTTP over TCP PoC) + +最終更新: 2025-08-22 + +## 概要 +- `nyash-net-plugin` は Socket/HTTP をプラグインとして提供します。 +- HTTP は最小限の HTTP/1.1 実装(GET/POST、ヘッダ、Content-Length)を実ソケットで処理します。 + +## 提供Box +- `SocketServerBox`, `SocketClientBox`, `SocketConnBox` +- `HttpServerBox`, `HttpRequestBox`, `HttpResponseBox`, `HttpClientBox` + +`nyash.toml` 定義例(抜粋)はリポジトリ直下の `nyash.toml` を参照。 + +## 動作仕様(HTTP) +- Server + - `start(port)`: TCP待受を開始 + - `accept()`: 接続受理+リクエスト簡易パース(path/body)→ `HttpRequestBox` を返す + - `HttpRequestBox.respond(resp)`: `resp` の status/header/body を HTTP/1.1 として送出(`Connection: close`) +- Client + - `get(url)`, `post(url, body)`: host:port に接続し、HTTP/1.1 リクエストを送出 + - `HttpResponseBox` は遅延受信。`readBody()/getStatus()/getHeader()` 呼び出し時に受信・パース + +制限: +- Chunked/Keep-Alive/HTTP/2は非対応(PoC)。Content-Length のみ対応。 + +## エラーモデル(PoC) +- BID-FFI の負値(例: `-5` PluginError)は Nyash 側で例外(`RuntimeFailure`)として扱われます。 +- タイムアウト系(Socket の `acceptTimeout/recvTimeout`)はエラーではなく、以下の値を返します: + - `acceptTimeout(ms)`: タイムアウト時は `void` + - `recvTimeout(ms)`: タイムアウト時は 空 `bytes`(長さ0の文字列) + +将来の整合: +- `ResultBox` での返却に対応する設計(`Ok(value)`/`Err(ErrorBox)`)を検討中。 +- `nyash.toml` のメソッド宣言に戻り値型(Result)を記載し、ランタイムで自動ラップする案。 + +## 並列E2E運用の注意 +- 各テストで異なるポートを使用(例: 8080/8081/8090/8091...) +- サーバの `stop()` と再起動時はポート開放の遅延に注意(短い待機を挟むと安定) +- ログを有効化して競合や順序の問題を診断可能 + +## ログ出力 +環境変数で簡易ログを有効化できます。 + +```bash +export NYASH_NET_LOG=1 +export NYASH_NET_LOG_FILE=net_plugin.log +``` + +stderr とファイルの両方に出力されます。 + +## 例 +```nyash +local srv, cli, r, req, resp +srv = new HttpServerBox() +srv.start(8080) + +cli = new HttpClientBox() +r = cli.get("http://localhost:8080/hello") + +req = srv.accept() +resp = new HttpResponseBox() +resp.setStatus(200) +resp.setHeader("X-Test", "V") +resp.write("OK") +req.respond(resp) + +// client side +print(r.getStatus()) +print(r.getHeader("X-Test")) +print(r.readBody()) +``` diff --git a/docs/reference/plugin-system/plugin_lifecycle.md b/docs/reference/plugin-system/plugin_lifecycle.md new file mode 100644 index 00000000..dc685cf5 --- /dev/null +++ b/docs/reference/plugin-system/plugin_lifecycle.md @@ -0,0 +1,53 @@ +# Plugin Lifecycle and Box RAII + +最終更新: 2025-08-22 + +## 概要 +NyashのBoxには「ユーザー定義Box」「ビルトインBox」「プラグインBox」があります。いずれもRAII(取得した資源は所有者の寿命で解放)に従いますが、プラグインBoxは共有やシングルトン運用があるため、追加ルールがあります。 + +## 共通ライフサイクル(ユーザー/ビルトイン/プラグイン) +- インスタンスの寿命が尽きると、強参照フィールド(public/private)に対し順に `fini()` が呼ばれ解放(weak はスキップ) +- `local` 変数のスコープを抜けると、そのスコープで生成されたインスタンスは解放対象 +- 明示的に `fini()` が呼ばれた場合も同様に後処理を実施 + +補足: +- これらは Nyash のスコープトラッカにより実施されます +- 解放順は生成の逆順(LIFO)で、カスケード `fini` を保証します + +## プラグインBoxの特則(シングルトン) +- シングルトン(`nyash.toml`) + - プラグインのBox型は `singleton = true` を宣言可能 + - ローダが起動時に `birth()` し、以後は同一ハンドルを共有して返却 + - シャットダウン時(`shutdown_plugins_v2()` など)に一括 `fini()` されます + +補足: +- Nyashは参照カウントを採用しません。解放は「スコープ終了」または「明示的`fini`」のみで決まります(自己責任モデル)。 +- プラグインBoxも同じルールです。スコープ終了時に`fini`され、以後の利用はエラー(Use after fini)。 +- 長寿命が必要なケースは「シングルトン」で運用してください(個別のBoxに特例は設けない)。 + +### 例: `nyash.toml` 抜粋 +```toml +[libraries."libnyash_counter_plugin.so".CounterBox] +type_id = 7 +singleton = true +``` + +## Net Plugin(HTTP/TCP)運用メモ +- ログ + - `NYASH_NET_LOG=1` で有効化、`NYASH_NET_LOG_FILE=net_plugin.log` 出力先 +- 並列実行とポート + - E2Eや並列CIではポート競合を避けるため、テスト毎にポートを明示(例: 8080, 8081, ...) + - サーバ終了タイミング(`stop()`/スコープ終了)とクライアント接続の順序に注意 + +## ベストプラクティス +- ユーザー/ビルトインBox + - フィールドの weak 指定(循環参照の解消)を活用 + - 必要に応じて明示 `fini()` を呼び、高価な資源(ファイル/ソケット等)を早期解放 +- プラグインBox + - シングルトン化が望ましい長寿命資源(サーバ、デバイス)に `singleton = true` + - 複数スコープで共有される可能性がある値は、スコープ終了時に自動 `fini` されないことを前提に設計 + - 終了前に `shutdown_plugins_v2()` を呼ぶと単一箇所で確実に `fini` を実行可能 + +## 実装参照 +- スコープ追跡: `src/scope_tracker.rs`(スコープ終了時の `fini` 呼出し、プラグインBox自動 `fini` 回避) +- プラグインローダ: `src/runtime/plugin_loader_v2.rs`(シングルトン生成・保持・シャットダウン、`PluginHandleInner::drop` の `fini`) diff --git a/docs/予定/ai-agent-challenge-strategy.md b/docs/予定/ai-agent-challenge-strategy.md new file mode 100644 index 00000000..22e1eecb --- /dev/null +++ b/docs/予定/ai-agent-challenge-strategy.md @@ -0,0 +1,62 @@ +# AI Agent Challenge 1日実装戦略 + +## 概要 +DEV.toのAI Agents Challenge(n8n + Bright Data)にNyashで参加する戦略。 +締切: 2025年8月31日 + +## 重要な発見 +- 他の参加者は普通にJavaScriptで実装している(WASMは不要だった!) +- n8nはノーコードツール、Webhookで簡単に連携可能 +- NetBoxプラグインが既にHTTP機能を提供 + +## 1日実装プラン + +### タイムライン +- 8:00-10:00: 基本設計・n8n理解 +- 10:00-13:00: Nyash実装(HTTPBox活用) +- 13:00-16:00: n8n連携・Bright Data統合 +- 16:00-19:00: デモアプリ完成 +- 19:00-21:00: 記事作成・動画録画 +- 21:00-23:00: 投稿・最終調整 + +### 実装アーキテクチャ +```nyash +// n8nブリッジ +box N8nBridge { + init { httpClient, workflows } + + triggerWorkflow(webhookUrl, data) { + return me.httpClient.post(webhookUrl, data) + } +} + +// 価格監視AIエージェント例 +box PriceMonitorAgent { + init { products, notifier } + + monitorPrices() { + // Bright Data経由でスクレイピング + // n8n経由で通知 + } +} +``` + +### 差別化ポイント +1. **Nyashという独自言語**での実装(創造性満点) +2. Everything is Box哲学によるエレガントな設計 +3. 既存のNetBoxプラグインを活用した高速開発 + +### 必要な準備 +- n8n無料アカウント作成 +- Bright Data $250クレジット取得 +- NetBoxプラグインのテスト修正完了 + +### リスクと対策 +- 時間制約 → シンプルな実装に集中 +- 技術学習 → Webhook連携のみに限定 +- デモ作成 → 録画で対応(ライブ不要) + +## 結論 +技術的には1日で実装可能。Nyashの知名度向上と$1,000の賞金獲得のチャンス。 + +最終更新: 2025-08-21 \ No newline at end of file diff --git a/http_test_log.txt b/http_test_log.txt new file mode 100644 index 00000000..648f0930 --- /dev/null +++ b/http_test_log.txt @@ -0,0 +1,48 @@ +HTTP E2Eテスト実行ログ (2025-08-21) +================================ + +【テスト実行コマンド】 +NYASH_NET_LOG=1 cargo test --features plugins --test e2e_plugin_net -- --nocapture + +【テスト結果サマリー】 +- 全5テスト失敗 +- エラー内容: Cannot call method 'respond' on non-instance type (VoidBox) + +【詳細なエラー解析】 + +1. 根本原因: HttpRequestBox作成失敗 + - HttpServerBox.start() → 成功(instance_id作成確認) + - server.accept() → VoidBoxを返す(本来はHttpRequestBoxを返すべき) + - VoidBox.respond() → エラー(VoidBoxにはrespond()メソッドがない) + +2. 主要なログ出力: + - 🎉 birth() success: HttpServerBox instance_id=5 + - 🎉 birth() success: HttpClientBox instance_id=6 + - ❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type + +3. プラグインシステムの挙動: + - プラグインロード: 成功 + - Box型の認識: 成功(type_id=20 for HttpServerBox等) + - メソッド呼び出し: 部分的成功(start/stopは動作、acceptが問題) + +【失敗したテスト一覧】 +1. e2e_http_stub_end_to_end +2. e2e_http_post_and_headers +3. e2e_http_multiple_requests_order +4. e2e_http_server_restart +5. e2e_http_server_shutdown_and_restart + +【Socket E2Eテストとの比較】 +- Socket E2Eテスト: 全て成功(2/2 passed) +- HTTP E2Eテスト: 全て失敗(0/5 passed) +- 結論: プラグインシステム自体は正常、HTTP実装固有の問題 + +【推測される問題】 +1. HTTP accept()メソッドの実装が未完成(PoC段階) +2. 実際のHTTPリクエスト受信処理が未実装 +3. HttpRequestBoxインスタンスの生成ロジックが欠落 + +【ChatGPT5さんへの報告事項】 +- VoidBoxエラーはaccept()メソッドの戻り値が原因 +- HTTPサーバーの実装は基本的なソケット処理が必要 +- Socket実装は安定しているので参考にできる可能性あり \ No newline at end of file diff --git a/http_test_raw_log.txt b/http_test_raw_log.txt new file mode 100644 index 00000000..bacdee54 --- /dev/null +++ b/http_test_raw_log.txt @@ -0,0 +1,459 @@ +warning: unused macro definition: `debug_trace` + --> src/interpreter/core.rs:35:14 + | +35 | macro_rules! debug_trace { + | ^^^^^^^^^^^ + | + = note: `#[warn(unused_macros)]` on by default + +warning: unexpected `cfg` condition value: `llvm` + --> src/backend/mod.rs:13:7 + | +13 | #[cfg(feature = "llvm")] + | ^^^^^^^^^^^^^^^^ + | + = note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend` + = help: consider adding `llvm` as a feature in `Cargo.toml` + = note: see for more information about checking conditional configuration + = note: `#[warn(unexpected_cfgs)]` on by default + +warning: unexpected `cfg` condition value: `llvm` + --> src/backend/mod.rs:23:7 + | +23 | #[cfg(feature = "llvm")] + | ^^^^^^^^^^^^^^^^ + | + = note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend` + = help: consider adding `llvm` as a feature in `Cargo.toml` + = note: see for more information about checking conditional configuration + +warning: unreachable statement + --> src/interpreter/objects.rs:109:9 + | +27 | / match nyash_args { +28 | | Ok(args) => { +29 | | // Handle generics: if user-defined and type arguments provided, specialize declaration +30 | | let mut target_class = class.to_string(); +... | +106 | | } + | |_________- any code following this `match` expression is unreachable, as all arms diverge +... +109 | return Err(RuntimeError::UndefinedClass { name: class.to_string() }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement + | + = note: `#[warn(unreachable_code)]` on by default + +warning: unreachable pattern + --> src/interpreter/objects.rs:231:13 + | +120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" | +121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => { + | |__________________________________________________________- matches all the relevant values +... +231 | "NullBox" => { + | ^^^^^^^^^ no value can reach this + | + = note: `#[warn(unreachable_patterns)]` on by default + +warning: unreachable pattern + --> src/interpreter/objects.rs:384:13 + | +120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" | +121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => { + | |__________________________________________________________- matches all the relevant values +... +384 | "FloatBox" => { + | ^^^^^^^^^^ no value can reach this + +warning: unreachable pattern + --> src/interpreter/objects.rs:484:13 + | +120 | / "StringBox" | "IntegerBox" | "BoolBox" | "ArrayBox" | "ResultBox" | +121 | | "ErrorBox" | "NullBox" | "FloatBox" | "MapBox" => { + | |__________________________________________________________- matches all the relevant values +... +484 | "MapBox" => { + | ^^^^^^^^ no value can reach this + +warning: `nyash-rust` (lib) generated 7 warnings +warning: unexpected `cfg` condition value: `llvm` + --> src/runner.rs:28:7 + | +28 | #[cfg(feature = "llvm")] + | ^^^^^^^^^^^^^^^^ + | + = note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend` + = help: consider adding `llvm` as a feature in `Cargo.toml` + = note: see for more information about checking conditional configuration + +warning: unexpected `cfg` condition value: `llvm` + --> src/runner.rs:568:15 + | +568 | #[cfg(feature = "llvm")] + | ^^^^^^^^^^^^^^^^ + | + = note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend` + = help: consider adding `llvm` as a feature in `Cargo.toml` + = note: see for more information about checking conditional configuration + +warning: unexpected `cfg` condition value: `llvm` + --> src/runner.rs:591:19 + | +591 | #[cfg(not(feature = "llvm"))] + | ^^^^^^^^^^^^^^^^ + | + = note: expected values for `feature` are: `all-examples`, `cli`, `default`, `dynamic-file`, `gui`, `gui-examples`, `plugins`, and `wasm-backend` + = help: consider adding `llvm` as a feature in `Cargo.toml` + = note: see for more information about checking conditional configuration + +warning: `nyash-rust` (bin "nyash") generated 3 warnings (1 duplicate) + +running 5 tests +🔍 create_box called for: HttpServerBox +🔍 Config loaded successfully +🔍 create_box called for: HttpServerBox +🔍 Config loaded successfully +🔍 create_box called for: HttpServerBox +🔍 Config loaded successfully +🔍 create_box called for: HttpServerBox +🔍 Config loaded successfully +🔍 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... +🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 Found library: libnyash_net_plugin.so for box type: HttpServerBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 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 read successfully +🔍 nyash.toml read successfully +🔍 nyash.toml read successfully +🔍 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) +🔍 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(8099) -> Integer(tag=5) +🔍 stdlib not initialized for method call +🔍 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=2 +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=2) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=2) +🔍 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(8095) -> Integer(tag=5) +🔍 stdlib not initialized for method call +🔍 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=3 +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=3) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=3) +🔍 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(8096) -> Integer(tag=5) +🔍 stdlib not initialized for method call +🔍 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=4 +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=4) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=4) +🔍 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(8097) -> Integer(tag=5) +🔍 stdlib not initialized for method call +🔍 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=5 +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=5) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpServerBox (id=5) +🔍 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(8098) -> Integer(tag=5) +🔍 stdlib not initialized for method call +🔍 create_box called for: HttpClientBox +🔍 Config loaded successfully +🔍 create_box called for: HttpClientBox +🔍 Config loaded successfully +🔍 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... +🔍 Found library: libnyash_net_plugin.so for box type: HttpClientBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 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 read successfully +🔍 nyash.toml read successfully +🔍 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) +🔍 stdlib not initialized for method call +🔍 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) +🔍 stdlib not initialized for method call +🔍 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) +🔍 stdlib not initialized for method call +🔍 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) +🔍 stdlib not initialized for method call +🔍 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) +🔍 stdlib not initialized for method call +🔍 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=6 +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=6) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=6) +🔍 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=27) -> String(tag=6) +🔍 stdlib not initialized for method call +🔍 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=7 +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=7) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=7) +🔍 DEBUG: execute_method_call - object type: PluginBoxV2, method: post +🔍 DEBUG: Checking StringBox downcast for type: PluginBoxV2 +🔍 DEBUG: StringBox downcast failed +🔍 execute_plugin_box_v2_method called: HttpClientBox.post +[PluginLoaderV2] Invoke HttpClientBox.post: resolving and encoding args (argc=2) +[PluginLoaderV2] arg[0]: String(len=35) -> String(tag=6) +[PluginLoaderV2] arg[1]: String(len=15) -> String(tag=6) +🔍 stdlib not initialized for method call +🔍 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=8 +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=8) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpClientBox (id=8) +🔍 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=21) -> String(tag=6) +🔍 stdlib not initialized for method call +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 Plugin invoke completed with result: 0 +🔍 plugin_invoke_v2 returned result: 0 +🔍 TLV output from plugin (len=4): [1, 0, 0, 0] +🔍 create_box called for: VoidBox +VoidBox is not a plugin box type +🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond +🔍 DEBUG: Checking StringBox downcast for type: VoidBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond +❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type +🔍 create_box called for: VoidBox +VoidBox is not a plugin box type +🔍 create_box called for: VoidBox +VoidBox is not a plugin box type +🔍 DEBUG: execute_method_call - object type: VoidBox, method: readBody +🔍 DEBUG: Checking StringBox downcast for type: VoidBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: readBody +❌ Interpreter error: Type error: Cannot call method 'readBody' on non-instance type +🔍 create_box called for: HttpResponseBox +🔍 Config loaded successfully +🔍 create_box called for: HttpResponseBox +🔍 Config loaded successfully + +thread 'e2e_http_post_and_headers' panicked at tests/e2e_plugin_net.rs:159:43: +exec failed: TypeError { message: "Cannot call method 'readBody' on non-instance type" } +🔍 Found library: libnyash_net_plugin.so for box type: HttpResponseBox +🔍 Plugin loaded successfully +🔍 Reading nyash.toml for type configuration... +🔍 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 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=9 +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=9) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=9) +🔍 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 +🔍 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=10 +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=10) +🔍 stdlib not initialized for method call +🔍 DEBUG: PluginBoxV2::share_box called for HttpResponseBox (id=10) +🔍 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 +🔍 DEBUG: execute_method_call - object type: VoidBox, method: path +🔍 DEBUG: Checking StringBox downcast for type: VoidBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: path +❌ Interpreter error: Type error: Cannot call method 'path' on non-instance type + +thread 'e2e_http_multiple_requests_order' panicked at tests/e2e_plugin_net.rs:186:43: +exec failed: TypeError { message: "Cannot call method 'path' on non-instance type" } +[PluginLoaderV2] Invoke HttpResponseBox.write: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: String(len=1) -> String(tag=6) +🔍 stdlib not initialized for method call +[PluginLoaderV2] Invoke HttpResponseBox.write: resolving and encoding args (argc=1) +[PluginLoaderV2] arg[0]: String(len=1) -> String(tag=6) +🔍 stdlib not initialized for method call +🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond +🔍 DEBUG: Checking StringBox downcast for type: VoidBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond +🔍 DEBUG: execute_method_call - object type: VoidBox, method: respond +🔍 DEBUG: Checking StringBox downcast for type: VoidBox +🔍 DEBUG: StringBox downcast failed +🔍 DEBUG: Reached non-instance type error for type: VoidBox, method: respond +❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type +❌ Interpreter error: Type error: Cannot call method 'respond' on non-instance type + +thread 'e2e_http_server_shutdown_and_restart' panicked at tests/e2e_plugin_net.rs:102:22: +exec1: TypeError { message: "Cannot call method 'respond' on non-instance type" } + +thread 'e2e_http_server_restart' panicked at tests/e2e_plugin_net.rs:80:43: +exec failed: TypeError { message: "Cannot call method 'respond' on non-instance type" } +error: test failed, to rerun pass `--test e2e_plugin_net` + + +running 5 tests +test e2e_http_stub_end_to_end ... FAILED +test e2e_http_post_and_headers ... FAILED +test e2e_http_multiple_requests_order ... FAILED +test e2e_http_server_shutdown_and_restart ... FAILED +test e2e_http_server_restart ... FAILED + +failures: + +failures: + e2e_http_multiple_requests_order + e2e_http_post_and_headers + e2e_http_server_restart + e2e_http_server_shutdown_and_restart + e2e_http_stub_end_to_end + +test result: FAILED. 0 passed; 5 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.81s \ No newline at end of file diff --git a/plugins/nyash-net-plugin/src/lib.rs b/plugins/nyash-net-plugin/src/lib.rs index bf4778b6..a0239e08 100644 --- a/plugins/nyash-net-plugin/src/lib.rs +++ b/plugins/nyash-net-plugin/src/lib.rs @@ -88,9 +88,10 @@ static SOCK_CONN_ID: AtomicU32 = AtomicU32::new(1); static SOCK_CLIENT_ID: AtomicU32 = AtomicU32::new(1); struct ServerState { - running: bool, + running: Arc, port: i32, - pending: VecDeque, // queue of request ids + pending: Arc>>, // queue of request ids + handle: Mutex>>, start_seq: u32, } @@ -98,12 +99,17 @@ struct RequestState { path: String, body: Vec, response_id: Option, + // For HTTP-over-TCP server: map to an active accepted socket to respond on + server_conn_id: Option, } struct ResponseState { status: i32, headers: HashMap, body: Vec, + // For HTTP-over-TCP client: associated socket connection id to read from + client_conn_id: Option, + parsed: bool, } struct ClientState; @@ -155,14 +161,54 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: match m { M_BIRTH => { let id = SERVER_ID.fetch_add(1, Ordering::Relaxed); - SERVER_INSTANCES.lock().unwrap().insert(id, ServerState { running: false, port: 0, pending: VecDeque::new(), start_seq: 0 }); + SERVER_INSTANCES.lock().unwrap().insert(id, ServerState { + running: Arc::new(AtomicBool::new(false)), + port: 0, + pending: Arc::new(Mutex::new(VecDeque::new())), + handle: Mutex::new(None), + start_seq: 0, + }); write_u32(id, res, res_len) } M_SERVER_START => { // args: TLV string/int (port) let port = tlv_parse_i32(slice(args, args_len)).unwrap_or(0); if let Some(s) = SERVER_INSTANCES.lock().unwrap().get_mut(&id) { - s.running = true; s.port = port; s.start_seq = SERVER_START_SEQ.fetch_add(1, Ordering::Relaxed); + s.port = port; s.start_seq = SERVER_START_SEQ.fetch_add(1, Ordering::Relaxed); + let running = s.running.clone(); + let pending = s.pending.clone(); + running.store(true, Ordering::SeqCst); + // Spawn HTTP listener thread (real TCP) + let handle = std::thread::spawn(move || { + let addr = format!("127.0.0.1:{}", port); + if let Ok(listener) = TcpListener::bind(addr) { + let _ = listener.set_nonblocking(true); + loop { + if !running.load(Ordering::SeqCst) { break; } + match listener.accept() { + Ok((mut stream, _)) => { + // Parse minimal HTTP request (GET/POST) + let _ = stream.set_read_timeout(Some(Duration::from_millis(2000))); + if let Some((path, body)) = read_http_request(&mut stream) { + // Store stream for later respond() + let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed); + 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: None, server_conn_id: Some(conn_id) }); + pending.lock().unwrap().push_back(req_id); + } else { + // Malformed; drop connection + } + } + Err(_) => { + std::thread::sleep(Duration::from_millis(10)); + } + } + } + } + }); + *s.handle.lock().unwrap() = Some(handle); } // mark active server *ACTIVE_SERVER_ID.lock().unwrap() = Some(id); @@ -170,7 +216,8 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: } M_SERVER_STOP => { if let Some(s) = SERVER_INSTANCES.lock().unwrap().get_mut(&id) { - s.running = false; + s.running.store(false, Ordering::SeqCst); + if let Some(h) = s.handle.lock().unwrap().take() { let _ = h.join(); } } // clear active if this server was active let mut active = ACTIVE_SERVER_ID.lock().unwrap(); @@ -182,7 +229,7 @@ unsafe fn server_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: for _ in 0..1000 { if let Some(req_id) = { let mut map = SERVER_INSTANCES.lock().unwrap(); - if let Some(s) = map.get_mut(&id) { s.pending.pop_front() } else { None } + if let Some(s) = map.get_mut(&id) { s.pending.lock().unwrap().pop_front() } else { None } } { return write_tlv_handle(T_REQUEST, req_id, res, res_len); } @@ -198,7 +245,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 }); + REQUESTS.lock().unwrap().insert(id, RequestState { path: String::new(), body: vec![], response_id: None, server_conn_id: None }); write_u32(id, res, res_len) } M_REQ_PATH => { @@ -218,17 +265,45 @@ unsafe fn request_invoke(m: u32, id: u32, _args: *const u8, _args_len: usize, re // Acquire request let mut rq_map = REQUESTS.lock().unwrap(); if let Some(rq) = rq_map.get_mut(&id) { - // Determine target response id: prefer existing client response id if present + // If request is backed by a real socket, write HTTP over that socket + if let Some(conn_id) = rq.server_conn_id { + drop(rq_map); + // Read response content from provided response handle + 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 } + }; + // Build minimal HTTP/1.1 response + 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(); + buf.extend_from_slice(format!("HTTP/1.1 {} {}\r\n", status, reason).as_bytes()); + 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"); + buf.extend_from_slice(b"\r\n"); + buf.extend_from_slice(&body); + // Write and close + if let Some(conn) = SOCK_CONNS.lock().unwrap().remove(&conn_id) { + if let Ok(mut s) = conn.stream.lock() { let _ = s.write_all(&buf); let _ = s.flush(); } + } + return write_tlv_void(res, res_len); + } + + // Stub fallback: copy into paired client-side response let target_id = if let Some(existing) = rq.response_id { existing } else { provided_resp_id }; rq.response_id = Some(target_id); drop(rq_map); // release before locking responses - - // Copy response content from provided_resp_id to 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![] }); + 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 = src_status; dst.headers = src_headers; dst.body = src_body; @@ -244,7 +319,7 @@ 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![] }); + RESPONSES.lock().unwrap().insert(id, ResponseState { status: 200, headers: HashMap::new(), body: vec![], client_conn_id: None, parsed: false }); write_u32(id, res, res_len) } M_RESP_SET_STATUS => { @@ -266,13 +341,35 @@ 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 => { + // If bound to a client connection, lazily read and parse + let mut need_parse = None; + { + if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { + if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; } + } else { return E_INV_HANDLE; } + } + if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); } if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { write_tlv_bytes(&rp.body, res, res_len) } else { E_INV_HANDLE } } M_RESP_GET_STATUS => { + let mut need_parse = None; + { + if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { + if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; } + } else { return E_INV_HANDLE; } + } + if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); } if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { write_tlv_i32(rp.status, res, res_len) } else { E_INV_HANDLE } } M_RESP_GET_HEADER => { if let Ok(name) = tlv_parse_string(slice(args, args_len)) { + let mut need_parse = None; + { + if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { + if rp.client_conn_id.is_some() && !rp.parsed { need_parse = rp.client_conn_id; } + } else { return E_INV_HANDLE; } + } + if let Some(conn_id) = need_parse { parse_client_response_into(id, conn_id); } if let Some(rp) = RESPONSES.lock().unwrap().get(&id) { let v = rp.headers.get(&name).cloned().unwrap_or_default(); return write_tlv_string(&v, res, res_len); @@ -294,33 +391,23 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: M_CLIENT_GET => { // args: TLV String(url) let url = tlv_parse_string(slice(args, args_len)).unwrap_or_default(); - let path = parse_path(&url); - let port_hint = parse_port(&url); - // Create Request - let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body: vec![], response_id: None }); - // Enqueue to server: prefer port match, else newest running - { - let mut servers = SERVER_INSTANCES.lock().unwrap(); - if let Some(ph) = port_hint { - if let Some((_, s)) = servers.iter_mut().find(|(_, s)| s.running && s.port == ph) { - s.pending.push_back(req_id); - } else { - if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) { - s.pending.push_back(req_id); - } - } - } else if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) { - s.pending.push_back(req_id); + let port = parse_port(&url).unwrap_or(80); + let host = parse_host(&url).unwrap_or_else(|| "127.0.0.1".to_string()); + let (_h, _p, req_bytes) = build_http_request("GET", &url, None); + let addr = format!("{}:{}", host, port); + match TcpStream::connect(addr) { + Ok(mut stream) => { + let _ = stream.write_all(&req_bytes); + let _ = stream.flush(); + let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed); + SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) }); + // Response handle + let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false }); + write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } + Err(_) => E_ERR, } - // Create Response handle for client side to read later - let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed); - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 200, headers: HashMap::new(), body: vec![] }); - // Link - if let Some(rq) = REQUESTS.lock().unwrap().get_mut(&req_id) { rq.response_id = Some(resp_id); } - // Return Handle(Response) - write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } M_CLIENT_POST => { // args: TLV String(url), Bytes body @@ -334,32 +421,22 @@ unsafe fn client_invoke(m: u32, id: u32, args: *const u8, args_len: usize, res: let (t2, s2, p2) = tlv_parse_entry_hdr(data, pos).map_err(|_| ()).or(Err(())).unwrap_or((0,0,0)); if t2 != 6 && t2 != 7 { return E_INV_ARGS; } let body = data[p2..p2+s2].to_vec(); - - let path = parse_path(&url); - let port_hint = parse_port(&url); - // Create Request - let req_id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); - REQUESTS.lock().unwrap().insert(req_id, RequestState { path, body, response_id: None }); - // Enqueue to server: prefer port match, else newest running - { - let mut servers = SERVER_INSTANCES.lock().unwrap(); - if let Some(ph) = port_hint { - if let Some((_, s)) = servers.iter_mut().find(|(_, s)| s.running && s.port == ph) { - s.pending.push_back(req_id); - } else { - if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) { - s.pending.push_back(req_id); - } - } - } else if let Some((_, s)) = servers.iter_mut().filter(|(_, s)| s.running).max_by_key(|(_, s)| s.start_seq) { - s.pending.push_back(req_id); + let port = parse_port(&url).unwrap_or(80); + let host = parse_host(&url).unwrap_or_else(|| "127.0.0.1".to_string()); + let (_h, _p, req_bytes) = build_http_request("POST", &url, Some(&body)); + let addr = format!("{}:{}", host, port); + match TcpStream::connect(addr) { + Ok(mut stream) => { + let _ = stream.write_all(&req_bytes); + let _ = stream.flush(); + let conn_id = SOCK_CONN_ID.fetch_add(1, Ordering::Relaxed); + SOCK_CONNS.lock().unwrap().insert(conn_id, SockConnState { stream: Mutex::new(stream) }); + let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed); + RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 0, headers: HashMap::new(), body: vec![], client_conn_id: Some(conn_id), parsed: false }); + write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } + Err(_) => E_ERR, } - // Create paired client Response - let resp_id = RESPONSE_ID.fetch_add(1, Ordering::Relaxed); - RESPONSES.lock().unwrap().insert(resp_id, ResponseState { status: 200, headers: HashMap::new(), body: vec![] }); - if let Some(rq) = REQUESTS.lock().unwrap().get_mut(&req_id) { rq.response_id = Some(resp_id); } - write_tlv_handle(T_RESPONSE, resp_id, res, res_len) } _ => E_INV_METHOD, } @@ -483,6 +560,127 @@ fn tlv_parse_entry_hdr(data: &[u8], pos: usize) -> Result<(u8,usize,usize), ()> Ok((tag,size,p)) } +// ===== HTTP helpers ===== +fn parse_host(url: &str) -> Option { + // http://host[:port]/... + if let Some(rest) = url.split("//").nth(1) { + let host_port = rest.split('/').next().unwrap_or(""); + let host = host_port.split(':').next().unwrap_or(""); + if !host.is_empty() { return Some(host.to_string()); } + } + None +} + +fn build_http_request(method: &str, url: &str, body: Option<&[u8]>) -> (String, String, Vec) { + let host = parse_host(url).unwrap_or_else(|| "127.0.0.1".to_string()); + let path = parse_path(url); + let mut buf = Vec::new(); + buf.extend_from_slice(format!("{} {} HTTP/1.1\r\n", method, &path).as_bytes()); + buf.extend_from_slice(format!("Host: {}\r\n", host).as_bytes()); + buf.extend_from_slice(b"User-Agent: nyash-net-plugin/0.1\r\n"); + match body { + Some(b) => { + buf.extend_from_slice(format!("Content-Length: {}\r\n", b.len()).as_bytes()); + buf.extend_from_slice(b"Content-Type: application/octet-stream\r\n"); + buf.extend_from_slice(b"Connection: close\r\n\r\n"); + buf.extend_from_slice(b); + } + None => { + buf.extend_from_slice(b"Connection: close\r\n\r\n"); + } + } + (host, path, buf) +} + +fn read_http_request(stream: &mut TcpStream) -> Option<(String, Vec)> { + let mut buf = Vec::with_capacity(1024); + let mut tmp = [0u8; 1024]; + // Read until we see CRLFCRLF + let header_end; + loop { + match stream.read(&mut tmp) { + Ok(0) => break, // EOF + Ok(n) => { + buf.extend_from_slice(&tmp[..n]); + if let Some(pos) = find_header_end(&buf) { header_end = pos; break; } + if buf.len() > 64 * 1024 { return None; } + } + Err(_) => return None, + } + } + // Parse request line and headers + let header = &buf[..header_end]; + let after = &buf[header_end+4..]; + let header_str = String::from_utf8_lossy(header); + let mut lines = header_str.split("\r\n"); + let request_line = lines.next().unwrap_or(""); + let mut parts = request_line.split_whitespace(); + let method = parts.next().unwrap_or(""); + let path = parts.next().unwrap_or("/").to_string(); + let mut content_length: usize = 0; + for line in lines { if let Some((k,v)) = line.split_once(':') { if k.eq_ignore_ascii_case("Content-Length") { content_length = v.trim().parse().unwrap_or(0); } } } + let mut body = after.to_vec(); + while body.len() < content_length { + match stream.read(&mut tmp) { Ok(0) => break, Ok(n) => body.extend_from_slice(&tmp[..n]), Err(_) => break } + } + if method == "GET" || method == "POST" { Some((path, body)) } else { None } +} + +fn find_header_end(buf: &[u8]) -> Option { + if buf.len() < 4 { return None; } + for i in 0..=buf.len()-4 { if &buf[i..i+4] == b"\r\n\r\n" { return Some(i); } } + None +} + +fn parse_client_response_into(resp_id: u32, conn_id: u32) { + // Read full response from socket and fill ResponseState + let mut status: i32 = 200; + let mut headers: HashMap = HashMap::new(); + let mut body: Vec = Vec::new(); + if let Some(conn) = SOCK_CONNS.lock().unwrap().remove(&conn_id) { + if let Ok(mut s) = conn.stream.lock() { + let _ = s.set_read_timeout(Some(Duration::from_millis(4000))); + let mut buf = Vec::with_capacity(2048); + let mut tmp = [0u8; 2048]; + let header_end; + loop { + match s.read(&mut tmp) { + Ok(0) => break, + Ok(n) => { + buf.extend_from_slice(&tmp[..n]); + if let Some(pos) = find_header_end(&buf) { header_end = pos; break; } + if buf.len() > 256 * 1024 { break; } + } + Err(_) => break, + } + } + if let Some(pos) = find_header_end(&buf) { + let header = &buf[..pos]; + let after = &buf[pos+4..]; + // Parse status line and headers + let header_str = String::from_utf8_lossy(header); + let mut lines = header_str.split("\r\n"); + if let Some(status_line) = lines.next() { + let mut sp = status_line.split_whitespace(); + let _ver = sp.next(); + if let Some(code_str) = sp.next() { status = code_str.parse::().unwrap_or(200); } + } + for line in lines { + if let Some((k,v)) = line.split_once(':') { headers.insert(k.trim().to_string(), v.trim().to_string()); } + } + body.extend_from_slice(after); + let need = headers.get("Content-Length").and_then(|v| v.parse::().ok()).unwrap_or(0); + while body.len() < need { + match s.read(&mut tmp) { Ok(0) => break, Ok(n) => body.extend_from_slice(&tmp[..n]), Err(_) => break } + } + } + } + } + if let Some(rp) = RESPONSES.lock().unwrap().get_mut(&resp_id) { + rp.status = status; rp.headers = headers; rp.body = body; rp.parsed = true; rp.client_conn_id = None; + } +} + // ===== Simple logger (enabled when NYASH_NET_LOG=1) ===== static LOG_ON: Lazy = Lazy::new(|| std::env::var("NYASH_NET_LOG").unwrap_or_default() == "1"); static LOG_PATH: Lazy = Lazy::new(|| std::env::var("NYASH_NET_LOG_FILE").unwrap_or_else(|_| "net_plugin.log".to_string())); diff --git a/src/config/nyash_toml_v2.rs b/src/config/nyash_toml_v2.rs index 5c3dd310..655296c7 100644 --- a/src/config/nyash_toml_v2.rs +++ b/src/config/nyash_toml_v2.rs @@ -61,6 +61,9 @@ pub struct MethodDefinition { /// Optional argument declarations (v2.1+) #[serde(default)] pub args: Option>, + /// Optional: wrap return and errors into ResultBox when true + #[serde(default)] + pub returns_result: bool, } /// Method argument declaration (v2.1+) diff --git a/src/debug/leak_tracker.rs b/src/debug/leak_tracker.rs new file mode 100644 index 00000000..50a59128 --- /dev/null +++ b/src/debug/leak_tracker.rs @@ -0,0 +1,38 @@ +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +static ENABLED: Lazy = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1"); +static LEAKS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +pub fn init() { + let _ = &*REPORTER; // ensure reporter is constructed +} + +pub fn register_plugin(box_type: &str, instance_id: u32) { + if !*ENABLED { return; } + let mut m = LEAKS.lock().unwrap(); + m.insert((box_type.to_string(), instance_id), "plugin"); +} + +pub fn finalize_plugin(box_type: &str, instance_id: u32) { + if !*ENABLED { return; } + let mut m = LEAKS.lock().unwrap(); + m.remove(&(box_type.to_string(), instance_id)); +} + +struct Reporter; +impl Drop for Reporter { + fn drop(&mut self) { + if !*ENABLED { return; } + let m = LEAKS.lock().unwrap(); + if m.is_empty() { return; } + eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len()); + for ((ty, id), _) in m.iter() { + eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id); + } + } +} + +static REPORTER: Lazy = Lazy::new(|| Reporter); + diff --git a/src/interpreter/core.rs b/src/interpreter/core.rs index 64a97265..e3b98e11 100644 --- a/src/interpreter/core.rs +++ b/src/interpreter/core.rs @@ -228,6 +228,9 @@ pub struct NyashInterpreter { /// 共有ランタイム(Boxレジストリ等) #[allow(dead_code)] pub(super) runtime: NyashRuntime, + + /// 現在の文脈で式結果が破棄されるか(must_use警告用) + pub(super) discard_context: bool, } impl NyashInterpreter { @@ -259,6 +262,7 @@ impl NyashInterpreter { invalidated_ids: Arc::new(Mutex::new(HashSet::new())), stdlib: None, // 遅延初期化 runtime, + discard_context: false, } } @@ -292,6 +296,7 @@ impl NyashInterpreter { invalidated_ids: Arc::new(Mutex::new(HashSet::new())), stdlib: None, runtime, + discard_context: false, } } @@ -321,6 +326,7 @@ impl NyashInterpreter { invalidated_ids: Arc::new(Mutex::new(HashSet::new())), stdlib: None, // 遅延初期化 runtime, + discard_context: false, } } @@ -351,6 +357,7 @@ impl NyashInterpreter { invalidated_ids: Arc::new(Mutex::new(HashSet::new())), stdlib: None, runtime, + discard_context: false, } } @@ -379,8 +386,12 @@ impl NyashInterpreter { ASTNode::Program { statements, .. } => { let mut result: Box = Box::new(VoidBox::new()); - for statement in statements.iter() { + let last = statements.len().saturating_sub(1); + for (i, statement) in statements.iter().enumerate() { + let prev = self.discard_context; + self.discard_context = i != last; // 最終文以外は値が破棄される result = self.execute_statement(statement)?; + self.discard_context = prev; // 制御フローチェック match &self.control_flow { diff --git a/src/interpreter/expressions/calls.rs b/src/interpreter/expressions/calls.rs index 69523a33..58a38910 100644 --- a/src/interpreter/expressions/calls.rs +++ b/src/interpreter/expressions/calls.rs @@ -996,6 +996,10 @@ impl NyashInterpreter { method: &str, arguments: &[ASTNode], ) -> Result, RuntimeError> { + // Guard: use-after-fini is a runtime error (明示ライフサイクル) + if plugin_box.is_finalized() { + return Err(RuntimeError::RuntimeFailure { message: format!("Use after fini: {}", plugin_box.box_type) }); + } eprintln!("🔍 execute_plugin_box_v2_method called: {}.{}", plugin_box.box_type, method); let mut arg_values: Vec> = Vec::new(); for arg in arguments { diff --git a/src/interpreter/statements.rs b/src/interpreter/statements.rs index cbc3e9e4..4c02739d 100644 --- a/src/interpreter/statements.rs +++ b/src/interpreter/statements.rs @@ -20,6 +20,26 @@ macro_rules! debug_trace { } impl NyashInterpreter { + fn warn_if_must_use(&self, value: &Box) { + if std::env::var("NYASH_LINT_MUSTUSE").unwrap_or_default() != "1" { return; } + if !self.discard_context { return; } + // 重資源のヒューリスティクス: プラグインBox、またはHTTP/Socket/File系の型名 + #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + { + if value.as_any().downcast_ref::().is_some() { + eprintln!("[lint:must_use] Discarded resource value (plugin box). Consider assigning it or calling fini()."); + return; + } + } + let ty = value.type_name(); + let heavy = matches!(ty, + "FileBox" | "SocketBox" | "SocketServerBox" | "SocketClientBox" | "SocketConnBox" | + "HTTPServerBox" | "HTTPRequestBox" | "HTTPResponseBox" | "HttpClientBox" + ); + if heavy { + eprintln!("[lint:must_use] Discarded {} value. Consider assigning it or calling fini().", ty); + } + } /// 文を実行 - Core statement execution engine pub(super) fn execute_statement(&mut self, statement: &ASTNode) -> Result, RuntimeError> { match statement { @@ -180,8 +200,12 @@ impl NyashInterpreter { Ok(Box::new(VoidBox::new())) } - // 式文 - _ => self.execute_expression(statement), + // 式文(結果は多くの場合破棄されるため、must_use警告を出力) + _ => { + let v = self.execute_expression(statement)?; + self.warn_if_must_use(&v); + Ok(v) + }, } } diff --git a/src/runtime/leak_tracker.rs b/src/runtime/leak_tracker.rs new file mode 100644 index 00000000..bd330395 --- /dev/null +++ b/src/runtime/leak_tracker.rs @@ -0,0 +1,34 @@ +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +static ENABLED: Lazy = Lazy::new(|| std::env::var("NYASH_LEAK_LOG").unwrap_or_default() == "1"); +static LEAKS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +pub fn init() { let _ = &*REPORTER; } + +pub fn register_plugin(box_type: &str, instance_id: u32) { + if !*ENABLED { return; } + LEAKS.lock().unwrap().insert((box_type.to_string(), instance_id), "plugin"); +} + +pub fn finalize_plugin(box_type: &str, instance_id: u32) { + if !*ENABLED { return; } + LEAKS.lock().unwrap().remove(&(box_type.to_string(), instance_id)); +} + +struct Reporter; +impl Drop for Reporter { + fn drop(&mut self) { + if !*ENABLED { return; } + let m = LEAKS.lock().unwrap(); + if m.is_empty() { return; } + eprintln!("[leak] Detected {} non-finalized plugin boxes:", m.len()); + for ((ty, id), _) in m.iter() { + eprintln!(" - {}(id={}) not finalized (missing fini or scope)", ty, id); + } + } +} + +static REPORTER: Lazy = Lazy::new(|| Reporter); + diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 1f578a0e..900c8ef3 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -5,6 +5,7 @@ pub mod plugin_config; pub mod box_registry; pub mod plugin_loader_v2; +pub mod leak_tracker; pub mod unified_registry; pub mod nyash_runtime; // pub mod plugin_box; // legacy - 古いPluginBox diff --git a/src/runtime/plugin_loader_v2.rs b/src/runtime/plugin_loader_v2.rs index 7c9a24af..6627ac8d 100644 --- a/src/runtime/plugin_loader_v2.rs +++ b/src/runtime/plugin_loader_v2.rs @@ -14,6 +14,7 @@ mod enabled { // use std::ffi::c_void; // unused use std::any::Any; use once_cell::sync::Lazy; + use crate::runtime::leak_tracker; /// Loaded plugin information pub struct LoadedPluginV2 { @@ -71,6 +72,7 @@ mod enabled { pub fn finalize_now(&self) { if let Some(fini_id) = self.fini_method_id { if !self.finalized.swap(true, std::sync::atomic::Ordering::SeqCst) { + leak_tracker::finalize_plugin("PluginBox", self.instance_id); let tlv_args: [u8; 4] = [1, 0, 0, 0]; let mut out: [u8; 4] = [0; 4]; let mut out_len: usize = out.len(); @@ -196,6 +198,7 @@ mod enabled { impl PluginBoxV2 { pub fn instance_id(&self) -> u32 { self.inner.instance_id } pub fn finalize_now(&self) { self.inner.finalize_now() } + pub fn is_finalized(&self) -> bool { self.inner.finalized.load(std::sync::atomic::Ordering::SeqCst) } } /// Plugin loader v2 @@ -376,6 +379,7 @@ impl PluginBoxV2 { 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); 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 = { @@ -497,15 +501,29 @@ impl PluginBoxV2 { if rc != 0 { let be = BidError::from_raw(rc); eprintln!("[PluginLoaderV2] invoke rc={} ({}) for {}.{}", rc, be.message(), box_type, method_name); + if returns_result { + let err = crate::exception_box::ErrorBox::new(&format!("{} (code: {})", be.message(), rc)); + return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_err(Box::new(err))))); + } return Err(be); } // Decode: BID-1 style header + first entry - let result = if out_len == 0 { None } else { + let result = if out_len == 0 { + if returns_result { + Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box) + } else { None } + } else { let data = &out[..out_len]; if data.len() < 4 { return Ok(None); } let _ver = u16::from_le_bytes([data[0], data[1]]); let argc = u16::from_le_bytes([data[2], data[3]]); - if argc == 0 { return Ok(None); } + if argc == 0 { + if returns_result { + return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))))); + } else { + return Ok(None); + } + } if data.len() < 8 { return Ok(None); } let tag = data[4]; let _rsv = data[5]; @@ -536,7 +554,12 @@ impl PluginBoxV2 { finalized: std::sync::atomic::AtomicBool::new(false), }), }; - return Ok(Some(Box::new(pbox) as Box)); + let val: Box = Box::new(pbox); + if returns_result { + return Ok(Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)))); + } else { + return Ok(Some(val)); + } } } } @@ -544,13 +567,17 @@ impl PluginBoxV2 { } 2 if size == 4 => { // I32 let mut b = [0u8;4]; b.copy_from_slice(payload); - Some(Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64)) as Box) + let val: Box = Box::new(IntegerBox::new(i32::from_le_bytes(b) as i64)); + 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(); - Some(Box::new(StringBox::new(s)) as Box) + let val: Box = Box::new(StringBox::new(s)); + if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(val)) as Box) } else { Some(val) } } - 9 => None, // Void + 9 => { + if returns_result { Some(Box::new(crate::boxes::result::NyashResultBox::new_ok(Box::new(crate::box_trait::VoidBox::new()))) as Box) } else { None } + }, _ => None, } }; @@ -723,7 +750,7 @@ impl PluginBoxV2 { }; eprintln!("🎉 birth() success: {} instance_id={}", box_type, instance_id); - + // Create v2 plugin box wrapper with actual instance_id let plugin_box = PluginBoxV2 { box_type: box_type.to_string(), @@ -735,6 +762,8 @@ impl PluginBoxV2 { finalized: std::sync::atomic::AtomicBool::new(false), }), }; + leak_tracker::init(); + leak_tracker::register_plugin(&plugin_box.box_type, instance_id); Ok(Box::new(plugin_box)) } diff --git a/src/scope_tracker.rs b/src/scope_tracker.rs index 7c30196b..199aae9a 100644 --- a/src/scope_tracker.rs +++ b/src/scope_tracker.rs @@ -39,9 +39,10 @@ impl ScopeTracker { let _ = instance.fini(); continue; } - // PluginBoxV2: do not auto-finalize (shared handle may be referenced elsewhere) + // PluginBoxV2: 明示ライフサイクルに合わせ、スコープ終了時にfini(自己責任運用) #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))] - if arc_box.as_any().downcast_ref::().is_some() { + if let Some(p) = arc_box.as_any().downcast_ref::() { + p.finalize_now(); continue; } // Builtin and others: no-op for now diff --git a/tests/e2e_plugin_net_additional.rs b/tests/e2e_plugin_net_additional.rs new file mode 100644 index 00000000..4642f1c0 --- /dev/null +++ b/tests/e2e_plugin_net_additional.rs @@ -0,0 +1,99 @@ +#![cfg(all(feature = "plugins", not(target_arch = "wasm32")))] + +use nyash_rust::parser::NyashParser; +use nyash_rust::runtime::plugin_loader_v2::{init_global_loader_v2, get_global_loader_v2}; +use nyash_rust::runtime::box_registry::get_global_registry; +use nyash_rust::runtime::PluginConfig; + +fn try_init_plugins() -> bool { + if !std::path::Path::new("nyash.toml").exists() { return false; } + if let Err(e) = init_global_loader_v2("nyash.toml") { eprintln!("init failed: {:?}", e); return false; } + let loader = get_global_loader_v2(); + let loader = loader.read().unwrap(); + if let Some(conf) = &loader.config { + let mut map = std::collections::HashMap::new(); + for (lib, def) in &conf.libraries { for b in &def.boxes { map.insert(b.clone(), lib.clone()); } } + get_global_registry().apply_plugin_config(&PluginConfig { plugins: map }); + true + } else { false } +} + +#[test] +fn e2e_http_two_servers_parallel() { + std::env::set_var("NYASH_NET_LOG", "1"); + std::env::set_var("NYASH_NET_LOG_FILE", "net_plugin.log"); + if !try_init_plugins() { return; } + + let code = r#" +local s1, s2, c, r1, r2, req1, req2, p1, p2, x, y +s1 = new HttpServerBox() +s2 = new HttpServerBox() +s1.start(8101) +s2.start(8102) + +c = new HttpClientBox() +r1 = c.get("http://localhost:8101/a") +r2 = c.get("http://localhost:8102/b") + +// accept once per pending request and keep handles +req1 = s1.accept() +req2 = s2.accept() +p1 = req1.path() +p2 = req2.path() + +x = new HttpResponseBox() +x.write("X") +y = new HttpResponseBox() +y.write("Y") + +// respond using kept request handles +req1.respond(x) +req2.respond(y) + +// read results +x = r1.readBody() +y = r2.readBody() +x + ":" + y +"#; + + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut i = nyash_rust::interpreter::NyashInterpreter::new(); + let result = i.execute(ast).expect("exec"); + assert!(result.to_string_box().value.contains(":")); +} + +#[test] +fn e2e_http_long_body_and_headers() { + std::env::set_var("NYASH_NET_LOG", "1"); + std::env::set_var("NYASH_NET_LOG_FILE", "net_plugin.log"); + if !try_init_plugins() { return; } + + let code = r#" +local s, c, r, q, resp, body, hv +s = new HttpServerBox() +s.start(8103) + +c = new HttpClientBox() +r = c.post("http://localhost:8103/long", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + +q = s.accept() +body = q.readBody() +resp = new HttpResponseBox() +resp.setStatus(202) +resp.setHeader("X-Alpha", "A") +resp.setHeader("X-Beta", "B") +resp.write("OK-LONG") +q.respond(resp) + +body = r.readBody() +hv = r.getHeader("X-Alpha") +hv + ":" + body +"#; + + let ast = NyashParser::parse_from_string(code).expect("parse"); + let mut i = nyash_rust::interpreter::NyashInterpreter::new(); + let result = i.execute(ast).expect("exec"); + let s = result.to_string_box().value; + assert!(s.starts_with("A:")); + assert!(s.contains("OK-LONG")); +}