diff --git a/docs/VM_README.md b/docs/VM_README.md index c648b19c..5cc2f1e2 100644 --- a/docs/VM_README.md +++ b/docs/VM_README.md @@ -35,3 +35,46 @@ NYASH_VM_STATS=1 NYASH_VM_STATS_JSON=1 ./target/debug/nyash --backend vm program ``` 出力は `total`(総命令数), `elapsed_ms`(経過時間), `counts`(命令種別→回数), `top20`(上位20種)を含みます。 + +## 既知の制約とTips(VM×プラグイン) +- Netプラグイン(HTTP) + - unreachable(接続不可/タイムアウト)は `Result.Err(ErrorBox)`。 + - HTTP 404/500 は `Result.Ok(Response)`(アプリ側で `response.status` を確認)。 + - デバッグ: `NYASH_NET_LOG=1 NYASH_NET_LOG_FILE=net_plugin.log`。 +- FileBox + - `close()` は `Ok(Void)`。`match Ok(_)` で受けるか、戻り値を無視してよい。 +- Handle(BoxRef)戻り + - TLV tag=8(type_id:u32, instance_id:u32)。Loaderが返り値typeに対応する `fini_method_id` を設定し `PluginBoxV2` を構築。 + - `scope_tracker` がスコープ終了時に `fini()` を呼ぶ(メモリ安全)。 +- 大きいボディ/多ヘッダー/タイムアウト + - 逐次拡張中。異常時の挙動は上記Result規約に従う。実行ログと `--vm-stats` を併用して診断。 + - SocketBox(VM) + - 基本API: `bind/listen/accept/connect/read/write/close/isServer/isConnected` + - タイムアウト: `acceptTimeout(ms)` は接続なしで `void`、`recvTimeout(ms)` は空文字を返す + - 簡易E2E: `local_tests/socket_timeout_server.nyash` と `socket_timeout_client.nyash` + - Void 比較の扱い(VM) + - `Void` は値を持たないため、`Eq/Ne` のみ有効。`Void == Void` は真、それ以外の型との `==` は偽(`!=` は真)。 + - 順序比較(`<, <=, >, >=`)は `TypeError`。 + +## E2E 実行例(HTTPのResult挙動) + +代表ケースを `tools/run_vm_stats.sh` で実行できます。`--vm-stats-json` により命令プロファイルも取得可能です。 + +```bash +# 別ターミナルでサーバ起動 +./target/release/nyash local_tests/http_server_statuses.nyash + +# クライアント(別ターミナル) +tools/run_vm_stats.sh local_tests/vm_stats_http_ok.nyash vm_stats_ok.json +tools/run_vm_stats.sh local_tests/vm_stats_http_404.nyash vm_stats_404.json +tools/run_vm_stats.sh local_tests/vm_stats_http_500.nyash vm_stats_500.json + +# 到達不能(サーバ不要) +tools/run_vm_stats.sh local_tests/vm_stats_http_err.nyash vm_stats_err.json +``` + +期待されるResultモデル +- unreachable(接続不可/タイムアウト): `Result.Err(ErrorBox)` +- 404/500 等のHTTPエラー: `Result.Ok(Response)`(アプリ側で `response.status` を評価) + +詳細: `docs/reference/architecture/mir-to-vm-mapping.md` と `docs/examples/http_result_patterns.md` を参照。 diff --git a/docs/reference/architecture/mir-to-vm-mapping.md b/docs/reference/architecture/mir-to-vm-mapping.md index 4f7c6574..65eebeea 100644 --- a/docs/reference/architecture/mir-to-vm-mapping.md +++ b/docs/reference/architecture/mir-to-vm-mapping.md @@ -15,7 +15,10 @@ - UnaryOp: Partial - `Neg`(int), `Not`(bool) のみ。`BitNot` は TODO。 - Compare: Partial - - Integer/ String の Eq/Ne/Lt/Le/Gt/Ge 対応。その他型は TypeError。 + - Integer/ String の Eq/Ne/Lt/Le/Gt/Ge 対応。Bool は Eq/Ne のみ。 + - Void は値を持たないため、比較は Eq/Ne のみ定義。 + - `Void == Void` は true、`Void != Void` は false + - `Void == X` は false、`Void != X` は true(順序比較は TypeError) - Load / Store: Implemented - 現状はVM内の値スロット操作(簡易)。 - Copy: Implemented @@ -23,6 +26,10 @@ ## 制御フロー - Branch / Jump / Return: Implemented + - Branchの条件は `Bool` を期待。以下の動的変換を許容: + - `Integer` → 非ゼロは true + - `BoxRef(BoolBox)` → 中身のbool + - `BoxRef(VoidBox)` → false(nullish false) - Phi: Implemented - `LoopExecutor` による選択実装(前BB情報を利用)。 @@ -75,6 +82,27 @@ --- +## Result / Err / Handle(BoxRef) 取り扱い(重要) + +目的: プラグイン呼び出し(BoxCall→PluginLoaderV2)およびVM内の戻り値で、ResultとBoxRef(Handle)を正しく扱うための合意事項。 + +- HTTP系(Netプラグイン)の約束 + - unreachable(接続不可/タイムアウト等): `Result.Err(ErrorBox)` にマップする。 + - HTTP 404/500 等のステータス異常: `Result.Ok(Response)` として返す(アプリ層で扱う)。 +- FileBox等のVoid戻り + - `close()` のような副作用のみのメソッドは `Ok(Void)` を返す。VMではVoidの実体は持たない。 +- Handle(BoxRef)戻り値 + - プラグインは TLV tag=8(Handle)で `type_id:u32, instance_id:u32` を返す。 + - Loader は返り値の `type_id` に対応する正しい `fini_method_id` を設定し、`PluginBoxV2` を生成してVMへ返す。 + - 注意: 生成元のBoxと返り値のBoxの型が異なるケースがあるため、「呼び出し元のfini値を流用しない」。必ず返り値 `type_id` を基に解決する。 +- Resultの整合 + - VMは `Result` をネイティブ表現し、`match` 等で分岐可能。 + - `Ok(Void)` は `match Ok(_)` と等価に扱える(Voidは値を持たない)。 + +参考: TLV/Loader仕様は `docs/reference/plugin-system/ffi-abi-specification.md` と `plugin-tester.md` を参照。 + +--- + ## 既知の課題(抜粋) - BinOp/UnaryOp/Compare の型拡張(浮動小数・Bool/Box等)。 - ArrayGet/ArraySet の実装。 @@ -83,6 +111,11 @@ - WeakRef/Barrier の実体化(必要性評価の上、命令ダイエット候補)。 - PluginBoxV2 のVM側統合強化(引数/戻り値のTLV全型対応、Handle戻り値→BoxRef化)。 +Verifier(検証)に関する追加事項(方針) +- use-before-def across merge の強化: merge後にphiが未使用/未定義を選択するパスを禁止。 +- if-merge の戻り: retはphiのdstを返す(実装済み)。 +- TypeOp(TypeCheck/Cast)と命令の整合: Verifierで型注釈に基づく不一致を検出。 + ## VM統計(計測) - `--vm-stats` / `--vm-stats-json` で命令ごとの使用回数と時間(ms)を出力。 - ホット命令抽出によりダイエット候補を定量化。 @@ -119,6 +152,11 @@ - Const/NewBoxが次点。定数・生成の最適化(定数畳み込み/軽量生成・シェアリング)が効果的。 - 異常系は早期収束で命令半減。if/phi修正は実戦で有効(Branch/Jump/Phiが顕在化)。 +補足(HTTP 404/500の比較) +- 404("Not Found")、500("Internal Server Error")ともに同一プロファイル(合計55命令) + - 内訳例: BoxCall: 23 / Const: 16 / NewBox: 12 / BinOp: 2 / Return: 1 / Safepoint: 1 +- ステータス差はアプリ層で完結し、VM命令の分布には有意差なし(設計の意図どおり) + ## 26命令ダイエット(検討のたたき台) 方針: 「命令の意味は保ちつつ集約」。代表案: - 維持: Const / Copy / Load / Store / BinOp / UnaryOp / Compare / Jump / Branch / Phi / Return / Call / BoxCall / NewBox / ArrayGet / ArraySet diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 6e497bb5..3b754718 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -963,6 +963,14 @@ impl VM { } }, + (VMValue::String(l), VMValue::Bool(r)) => { + // String + Bool concatenation + match op { + BinaryOp::Add => Ok(VMValue::String(format!("{}{}", l, r))), + _ => Err(VMError::TypeError("String-bool operations only support addition".to_string())), + } + }, + (VMValue::String(l), VMValue::String(r)) => { // String concatenation match op { @@ -1239,6 +1247,113 @@ impl VM { } } + // ArrayBox methods (minimal set) + if let Some(array_box) = box_value.as_any().downcast_ref::() { + match method { + "push" => { + if let Some(v) = _args.get(0) { return Ok(array_box.push(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: push(value) requires 1 arg"))); + }, + "pop" => { return Ok(array_box.pop()); }, + "length" | "len" => { return Ok(array_box.length()); }, + "get" => { + if let Some(i) = _args.get(0) { return Ok(array_box.get(i.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: get(index) requires 1 arg"))); + }, + "set" => { + if _args.len() >= 2 { return Ok(array_box.set(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: set(index, value) requires 2 args"))); + }, + "remove" => { + if let Some(i) = _args.get(0) { return Ok(array_box.remove(i.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: remove(index) requires 1 arg"))); + }, + "contains" => { + if let Some(v) = _args.get(0) { return Ok(array_box.contains(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: contains(value) requires 1 arg"))); + }, + "indexOf" => { + if let Some(v) = _args.get(0) { return Ok(array_box.indexOf(v.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: indexOf(value) requires 1 arg"))); + }, + "clear" => { return Ok(array_box.clear()); }, + "join" => { + if let Some(sep) = _args.get(0) { return Ok(array_box.join(sep.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: join(sep) requires 1 arg"))); + }, + "sort" => { return Ok(array_box.sort()); }, + "reverse" => { return Ok(array_box.reverse()); }, + "slice" => { + if _args.len() >= 2 { return Ok(array_box.slice(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: slice(start, end) requires 2 args"))); + }, + _ => return Ok(Box::new(VoidBox::new())), + } + } + + // MapBox methods (minimal set) + if let Some(map_box) = box_value.as_any().downcast_ref::() { + match method { + "set" => { + if _args.len() >= 2 { return Ok(map_box.set(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: set(key, value) requires 2 args"))); + }, + "get" => { + if let Some(k) = _args.get(0) { return Ok(map_box.get(k.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: get(key) requires 1 arg"))); + }, + "has" => { + if let Some(k) = _args.get(0) { return Ok(map_box.has(k.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: has(key) requires 1 arg"))); + }, + "delete" | "remove" => { + if let Some(k) = _args.get(0) { return Ok(map_box.delete(k.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: delete(key) requires 1 arg"))); + }, + "keys" => { return Ok(map_box.keys()); }, + "values" => { return Ok(map_box.values()); }, + "size" => { return Ok(map_box.size()); }, + "clear" => { return Ok(map_box.clear()); }, + _ => return Ok(Box::new(VoidBox::new())), + } + } + + // SocketBox methods (minimal set + timeout variants) + if let Some(sock) = box_value.as_any().downcast_ref::() { + match method { + "bind" => { + if _args.len() >= 2 { return Ok(sock.bind(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: bind(address, port) requires 2 args"))); + }, + "listen" => { + if let Some(b) = _args.get(0) { return Ok(sock.listen(b.clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: listen(backlog) requires 1 arg"))); + }, + "accept" => { return Ok(sock.accept()); }, + "acceptTimeout" | "accept_timeout" => { + if let Some(ms) = _args.get(0) { return Ok(sock.accept_timeout(ms.clone_or_share())); } + return Ok(Box::new(crate::box_trait::VoidBox::new())); + }, + "connect" => { + if _args.len() >= 2 { return Ok(sock.connect(_args[0].clone_or_share(), _args[1].clone_or_share())); } + return Ok(Box::new(StringBox::new("Error: connect(address, port) requires 2 args"))); + }, + "read" => { return Ok(sock.read()); }, + "recvTimeout" | "recv_timeout" => { + if let Some(ms) = _args.get(0) { return Ok(sock.recv_timeout(ms.clone_or_share())); } + return Ok(Box::new(StringBox::new(""))); + }, + "write" => { + if let Some(d) = _args.get(0) { return Ok(sock.write(d.clone_or_share())); } + return Ok(Box::new(crate::box_trait::BoolBox::new(false))); + }, + "close" => { return Ok(sock.close()); }, + "isServer" | "is_server" => { return Ok(sock.is_server()); }, + "isConnected" | "is_connected" => { return Ok(sock.is_connected()); }, + _ => return Ok(Box::new(VoidBox::new())), + } + } + // IntegerBox methods if let Some(integer_box) = box_value.as_any().downcast_ref::() { match method { diff --git a/src/boxes/socket_box.rs b/src/boxes/socket_box.rs index 4882c6c1..f773c2fe 100644 --- a/src/boxes/socket_box.rs +++ b/src/boxes/socket_box.rs @@ -208,6 +208,46 @@ impl SocketBox { Box::new(BoolBox::new(false)) } } + + /// クライアント接続を受諾(タイムアウトms、タイムアウト時はvoid) + pub fn accept_timeout(&self, timeout_ms: Box) -> Box { + let ms = timeout_ms.to_string_box().value.parse::().unwrap_or(0); + if ms == 0 { return self.accept(); } + + let start = std::time::Instant::now(); + if let Ok(mut guard) = self.listener.write() { + if let Some(ref listener) = *guard { + let _ = listener.set_nonblocking(true); + loop { + match listener.accept() { + Ok((stream, _addr)) => { + let _ = listener.set_nonblocking(false); + drop(guard); + let client_socket = SocketBox::new(); + *client_socket.stream.write().unwrap() = Some(stream); + *client_socket.is_connected.write().unwrap() = true; + return Box::new(client_socket); + }, + Err(e) => { + if e.kind() == std::io::ErrorKind::WouldBlock { + if start.elapsed() >= Duration::from_millis(ms) { + let _ = listener.set_nonblocking(false); + return Box::new(crate::box_trait::VoidBox::new()); + } + std::thread::sleep(Duration::from_millis(5)); + continue; + } else { + eprintln!("🚨 SocketBox accept_timeout error: {}", e); + let _ = listener.set_nonblocking(false); + return Box::new(crate::box_trait::VoidBox::new()); + } + } + } + } + } + } + Box::new(crate::box_trait::VoidBox::new()) + } /// サーバーに接続(クライアントモード) pub fn connect(&self, address: Box, port: Box) -> Box { @@ -272,6 +312,44 @@ impl SocketBox { Box::new(StringBox::new("".to_string())) } } + + /// タイムアウト付き読み取り(ms)。タイムアウト時は空文字。 + pub fn recv_timeout(&self, timeout_ms: Box) -> Box { + let ms = timeout_ms.to_string_box().value.parse::().unwrap_or(0); + let stream_guard = self.stream.write().unwrap(); + if let Some(ref stream) = *stream_guard { + match stream.try_clone() { + Ok(mut stream_clone) => { + drop(stream_guard); + let _ = stream_clone.set_read_timeout(Some(Duration::from_millis(ms))); + let mut reader = BufReader::new(stream_clone); + let mut buffer = String::new(); + match reader.read_line(&mut buffer) { + Ok(_) => { + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { buffer.pop(); } + } + Box::new(StringBox::new(&buffer)) + } + Err(e) => { + if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut { + return Box::new(StringBox::new("")); + } + eprintln!("🚨 SocketBox recv_timeout error: {}", e); + Box::new(StringBox::new("")) + } + } + } + Err(e) => { + eprintln!("🚨 SocketBox stream clone error: {}", e); + Box::new(StringBox::new("")) + } + } + } else { + Box::new(StringBox::new("")) + } + } /// HTTP request を読み取り(ヘッダーまで含む) pub fn read_http_request(&self) -> Box { @@ -507,4 +585,4 @@ impl Drop for SocketBox { // Ensure sockets are properly closed let _ = self.close(); } -} \ No newline at end of file +}