diff --git a/Cargo.toml b/Cargo.toml index 2c2e3380..11bdbb31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ e2e = [] cli = [] plugins-only = [] builtin-core = [] +builtin-filebox = [] # Enable built-in FileBox provider (SSOT) ## Silence check-cfg warnings for historical cfg guards (kept off by default) vm-legacy = [] phi-legacy = [] diff --git a/docs/development/runtime/FILEBOX_PROVIDER.md b/docs/development/runtime/FILEBOX_PROVIDER.md new file mode 100644 index 00000000..64c8b85d --- /dev/null +++ b/docs/development/runtime/FILEBOX_PROVIDER.md @@ -0,0 +1,45 @@ +# File I/O Provider Architecture (Core‑RO + Plug‑in) + +目的 +- File I/O の意味論を単一起源(SSOT)に集約し、コアとプラグインの二重実装・分岐をなくす。 +- Analyzer/テスト/CI は FileBox に依存せず動作(--source-file で直接テキストを渡す)。必要時はコアの read‑only 実装で安全に実行。 +- 開発/本番はプラグイン(拡張版)を優先し、無い場合はコアにフォールバック。 + +設計(3層) +- リング0(SSOT / 共有抽象) + - `src/boxes/file/provider.rs` + - `trait FileIo { open(&str), read(), close(), caps(), … }` + - `struct FileCaps { read: bool, write: bool }` + - 共通エラー型・正規化ヘルパ(改行など) + - 役割: File I/O の“意味論”の唯一の真実源。 +- リング1(コア最小実装 + 薄いラッパ) + - `src/boxes/file/core_ro.rs` … `CoreRoFileIo`(read‑only; write は Fail‑Fast) + - `src/boxes/file/box_shim.rs` … `FileBox { provider: Arc }`(委譲のみ; 分岐ロジックなし) +- リング1(選択ポリシー) + - `src/runner/modes/common_util/provider_registry.rs` + - `select_file_provider(mode: auto|core-ro|plugin-only) -> Arc` + - 選択はここに閉じ込める(FileBox から分岐を排除) +- リング2(プラグイン実装) + - `plugins/nyash-filebox-plugin/src/provider.rs` … `PluginFileIo`(write/拡張を実装) + - SSOT の `FileIo` を実装し、共通意味論に従う + +モードと運用 +- `NYASH_FILEBOX_MODE=auto|core-ro|plugin-only` + - auto(既定): プラグインがあれば PluginFileIo、無ければ CoreRoFileIo + - core-ro: 常に CoreRoFileIo(Analyzer/CI 向け) + - plugin-only: プラグイン必須(無い場合は Fail‑Fast) +- `NYASH_DISABLE_PLUGINS=1` のときは自動で core‑ro を選択 +- Analyzer/テスト/CI + - `--source-file ` を第一経路(FileBox 非依存) + - json‑lsp の stdout は純 JSON(ログは stderr) + +利点 +- File I/O の重複・分岐が provider_registry に一元化され、コードの責務が明確。 +- Analyzer/テストはプラグイン非依存で安定(ノイズやロード失敗の影響を受けない)。 +- 本番は拡張可能なプラグインを優先しつつ、不在時はコアにフォールバック。 + +テスト方針 +- CoreRo: open/read/close 正常、存在しないパスで Fail‑Fast +- Plugin‑only: write を含む拡張 API 正常(プラグインが無い場合は Fail‑Fast) +- Auto: プラグイン不在時に CoreRo へフォールバック +- Analyzer: 全 HC テストが json‑lsp で緑、stdout は純 JSON を維持 diff --git a/src/boxes/basic/file_box.rs b/src/boxes/basic/file_box.rs index 99f81837..5f10ccb7 100644 --- a/src/boxes/basic/file_box.rs +++ b/src/boxes/basic/file_box.rs @@ -1,120 +1,12 @@ -//! FileBox - File system operations in Nyash +//! DEPRECATED: Use `crate::boxes::file::FileBox` instead //! -//! Implements the FileBox type with file I/O operations. +//! This module is kept for backward compatibility only. +//! All new code should use the SSOT provider-based FileBox implementation. -use crate::box_trait::{NyashBox, BoxCore, BoxBase, StringBox, BoolBox, VoidBox}; -use std::any::Any; -use std::fmt::{Debug, Display}; -use std::fs; -use std::path::Path; +#![deprecated( + since = "0.1.0", + note = "Use crate::boxes::file::FileBox instead. This module will be removed in a future version." +)] -/// File values in Nyash - file system operations -#[derive(Debug, Clone)] -pub struct FileBox { - pub path: String, - base: BoxBase, -} - -impl FileBox { - pub fn new(path: impl Into) -> Self { - Self { - path: path.into(), - base: BoxBase::new(), - } - } - - // ===== File Methods for Nyash ===== - - /// Read file contents as string - pub fn read(&self) -> Box { - match fs::read_to_string(&self.path) { - Ok(content) => Box::new(StringBox::new(content)), - Err(_) => Box::new(VoidBox::new()), // Return void on error for now - } - } - - /// Write content to file - pub fn write(&self, content: Box) -> Box { - let content_str = content.to_string_box().value; - match fs::write(&self.path, content_str) { - Ok(_) => Box::new(BoolBox::new(true)), - Err(_) => Box::new(BoolBox::new(false)), - } - } - - /// Check if file exists - pub fn exists(&self) -> Box { - Box::new(BoolBox::new(Path::new(&self.path).exists())) - } - - /// Delete file - pub fn delete(&self) -> Box { - match fs::remove_file(&self.path) { - Ok(_) => Box::new(BoolBox::new(true)), - Err(_) => Box::new(BoolBox::new(false)), - } - } - - /// Copy file to destination - pub fn copy(&self, dest_path: &str) -> Box { - match fs::copy(&self.path, dest_path) { - Ok(_) => Box::new(BoolBox::new(true)), - Err(_) => Box::new(BoolBox::new(false)), - } - } -} - -impl BoxCore for FileBox { - fn box_id(&self) -> u64 { - self.base.id - } - - fn parent_type_id(&self) -> Option { - self.base.parent_type_id - } - - fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "", self.path) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -impl NyashBox for FileBox { - fn to_string_box(&self) -> StringBox { - StringBox::new(format!("", self.path)) - } - - fn equals(&self, other: &dyn NyashBox) -> BoolBox { - if let Some(other_file) = other.as_any().downcast_ref::() { - BoolBox::new(self.path == other_file.path) - } else { - BoolBox::new(false) - } - } - - fn type_name(&self) -> &'static str { - "FileBox" - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - - /// 仮実装: clone_boxと同じ(後で修正) - fn share_box(&self) -> Box { - self.clone_box() - } -} - -impl Display for FileBox { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.fmt_box(f) - } -} \ No newline at end of file +// Re-export the new FileBox implementation for backward compatibility +pub use crate::boxes::file::FileBox; \ No newline at end of file diff --git a/src/boxes/file/box_shim.rs b/src/boxes/file/box_shim.rs new file mode 100644 index 00000000..ff2c25d0 --- /dev/null +++ b/src/boxes/file/box_shim.rs @@ -0,0 +1,24 @@ +//! Thin FileBox shim that delegates to a selected provider. +//! Not wired into the registry yet (safe placeholder). + +use std::sync::Arc; +use super::provider::{FileIo, FileCaps, FileResult}; + +#[allow(dead_code)] +pub struct FileBoxShim { + provider: Arc, + caps: FileCaps, +} + +#[allow(dead_code)] +impl FileBoxShim { + pub fn new(provider: Arc) -> Self { + let caps = provider.caps(); + Self { provider, caps } + } + pub fn open(&self, path: &str) -> FileResult<()> { self.provider.open(path) } + pub fn read(&self) -> FileResult { self.provider.read() } + pub fn close(&self) -> FileResult<()> { self.provider.close() } + pub fn caps(&self) -> FileCaps { self.caps } +} + diff --git a/src/boxes/file/core_ro.rs b/src/boxes/file/core_ro.rs new file mode 100644 index 00000000..af110fd4 --- /dev/null +++ b/src/boxes/file/core_ro.rs @@ -0,0 +1,50 @@ +//! Core read‑only File I/O provider (ring‑1). +//! Provides basic read-only file operations using std::fs::File. + +use super::provider::{FileCaps, FileError, FileIo, FileResult, normalize_newlines}; +use std::fs::File; +use std::io::Read; +use std::sync::RwLock; + +pub struct CoreRoFileIo { + handle: RwLock>, +} + +impl CoreRoFileIo { + pub fn new() -> Self { + Self { + handle: RwLock::new(None), + } + } +} + +impl FileIo for CoreRoFileIo { + fn caps(&self) -> FileCaps { + FileCaps::read_only() + } + + fn open(&self, path: &str) -> FileResult<()> { + let file = File::open(path) + .map_err(|e| FileError::Io(format!("Failed to open {}: {}", path, e)))?; + *self.handle.write().unwrap() = Some(file); + Ok(()) + } + + fn read(&self) -> FileResult { + let mut handle = self.handle.write().unwrap(); + if let Some(ref mut file) = *handle { + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| FileError::Io(format!("Read failed: {}", e)))?; + Ok(normalize_newlines(&content)) + } else { + Err(FileError::Io("No file opened".to_string())) + } + } + + fn close(&self) -> FileResult<()> { + *self.handle.write().unwrap() = None; + Ok(()) + } +} + diff --git a/src/boxes/file/mod.rs b/src/boxes/file/mod.rs index 80134432..a8acdc53 100644 --- a/src/boxes/file/mod.rs +++ b/src/boxes/file/mod.rs @@ -2,80 +2,80 @@ // Nyashの箱システムによるファイル入出力を提供します。 // 参考: 既存Boxの設計思想 -use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox}; -use std::any::Any; -use std::fs::{File, OpenOptions}; -use std::io::{Read, Result, Write}; -use std::sync::RwLock; +// SSOT provider design (ring‑0/1) — modules are currently placeholders +pub mod provider; // trait FileIo / FileCaps / FileError +pub mod core_ro; // Core read‑only provider +pub mod box_shim; // Thin delegating shim + +use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox}; +use crate::runtime::provider_lock; +use std::any::Any; +use std::sync::Arc; + +use self::provider::FileIo; -#[derive(Debug)] pub struct FileBox { - file: RwLock, + provider: Option>, path: String, base: BoxBase, } +impl std::fmt::Debug for FileBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FileBox") + .field("path", &self.path) + .field("provider", &"") + .finish() + } +} + impl Clone for FileBox { fn clone(&self) -> Self { - // File handles can't be easily cloned, so we'll reopen the file - match Self::open(&self.path) { - Ok(new_file_box) => new_file_box, - Err(_) => { - // Fallback to default if reopening fails - Self::new() - } + // Clone by copying provider reference and path + FileBox { + provider: self.provider.clone(), + path: self.path.clone(), + base: BoxBase::new(), } } } impl FileBox { pub fn new() -> Self { - // Create a default FileBox for delegation dispatch - // Uses a temporary file for built-in Box inheritance dispatch - let temp_path = "/tmp/nyash_temp_file"; - match Self::open(temp_path) { - Ok(file_box) => file_box, - Err(_) => { - // Fallback: create with empty file handle - only for dispatch - use std::fs::OpenOptions; - let file = OpenOptions::new() - .create(true) - .write(true) - .read(true) - .open("/dev/null") - .unwrap_or_else(|_| File::open("/dev/null").unwrap()); - FileBox { - file: RwLock::new(file), - path: String::new(), - base: BoxBase::new(), - } - } + FileBox { + provider: provider_lock::get_filebox_provider().cloned(), + path: String::new(), + base: BoxBase::new(), } } - pub fn open(path: &str) -> Result { - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(path)?; + pub fn open(path: &str) -> Result { + let provider = provider_lock::get_filebox_provider() + .ok_or("FileBox provider not initialized")? + .clone(); + + provider.open(path) + .map_err(|e| format!("Failed to open: {}", e))?; + Ok(FileBox { - file: RwLock::new(file), + provider: Some(provider), path: path.to_string(), base: BoxBase::new(), }) } - pub fn read_to_string(&self) -> Result { - let mut file = self.file.write().unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s)?; - Ok(s) + pub fn read_to_string(&self) -> Result { + if let Some(ref provider) = self.provider { + provider.read() + .map_err(|e| format!("Read failed: {}", e)) + } else { + Err("No provider available".to_string()) + } } - pub fn write_all(&self, buf: &[u8]) -> Result<()> { - let mut file = self.file.write().unwrap(); - file.write_all(buf) + pub fn write_all(&self, _buf: &[u8]) -> Result<(), String> { + // CoreRo does not support write - Fail-Fast + Err("Write operation not supported in read-only mode".to_string()) } /// ファイルの内容を読み取る @@ -87,12 +87,9 @@ impl FileBox { } /// ファイルに内容を書き込む - pub fn write(&self, content: Box) -> Box { - let content_str = content.to_string_box().value; - match self.write_all(content_str.as_bytes()) { - Ok(()) => Box::new(StringBox::new("ok")), - Err(e) => Box::new(StringBox::new(&format!("Error writing file: {}", e))), - } + pub fn write(&self, _content: Box) -> Box { + // Fail-Fast: CoreRo does not support write + Box::new(StringBox::new("Error: Write operation not supported in read-only mode")) } /// ファイルが存在するかチェック @@ -103,18 +100,14 @@ impl FileBox { /// ファイルを削除 pub fn delete(&self) -> Box { - match std::fs::remove_file(&self.path) { - Ok(()) => Box::new(StringBox::new("ok")), - Err(e) => Box::new(StringBox::new(&format!("Error deleting file: {}", e))), - } + // Fail-Fast: CoreRo does not support delete + Box::new(StringBox::new("Error: Delete operation not supported in read-only mode")) } /// ファイルをコピー - pub fn copy(&self, dest: &str) -> Box { - match std::fs::copy(&self.path, dest) { - Ok(_) => Box::new(StringBox::new("ok")), - Err(e) => Box::new(StringBox::new(&format!("Error copying file: {}", e))), - } + pub fn copy(&self, _dest: &str) -> Box { + // Fail-Fast: CoreRo does not support copy + Box::new(StringBox::new("Error: Copy operation not supported in read-only mode")) } } @@ -142,11 +135,8 @@ impl BoxCore for FileBox { impl NyashBox for FileBox { fn clone_box(&self) -> Box { - // Note: Cannot truly clone a File handle, so create a new one to the same path - match FileBox::open(&self.path) { - Ok(new_file) => Box::new(new_file), - Err(_) => Box::new(crate::box_trait::VoidBox::new()), // Return void on error - } + // Clone by copying provider and path reference + Box::new(self.clone()) } /// 仮実装: clone_boxと同じ(後で修正) diff --git a/src/boxes/file/provider.rs b/src/boxes/file/provider.rs new file mode 100644 index 00000000..ddc046b1 --- /dev/null +++ b/src/boxes/file/provider.rs @@ -0,0 +1,48 @@ +//! File I/O provider SSOT (trait + shared types) +//! +//! This module defines the unified File I/O abstraction used by both the +//! core read‑only implementation and the plugin implementation. + +/// File capabilities (minimal flag set) +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub struct FileCaps { + pub read: bool, + pub write: bool, +} + +impl FileCaps { + pub const fn read_only() -> Self { Self { read: true, write: false } } +} + +/// Unified error type (thin placeholder for now) +#[derive(thiserror::Error, Debug)] +pub enum FileError { + #[error("io error: {0}")] + Io(String), + #[error("unsupported operation")] + Unsupported, +} + +pub type FileResult = Result; + +/// Single source of truth for File I/O semantics +pub trait FileIo: Send + Sync { + fn caps(&self) -> FileCaps; + fn open(&self, path: &str) -> FileResult<()>; + fn read(&self) -> FileResult; + fn close(&self) -> FileResult<()>; + // Future: write/exists/stat … +} + +/// Normalize newlines to LF (optional helper) +#[allow(dead_code)] +pub fn normalize_newlines(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for b in s.as_bytes() { + if *b == b'\r' { continue; } + out.push(*b as char); + } + out +} + diff --git a/src/runner/modes/common_util/mod.rs b/src/runner/modes/common_util/mod.rs index 24d058a9..8e93faf6 100644 --- a/src/runner/modes/common_util/mod.rs +++ b/src/runner/modes/common_util/mod.rs @@ -13,3 +13,4 @@ pub mod exec; pub mod core_bridge; pub mod hako; pub mod plugin_guard; +pub mod provider_registry; diff --git a/src/runner/modes/common_util/provider_registry.rs b/src/runner/modes/common_util/provider_registry.rs new file mode 100644 index 00000000..49d92591 --- /dev/null +++ b/src/runner/modes/common_util/provider_registry.rs @@ -0,0 +1,37 @@ +//! Provider registry: selects concrete providers for core resources (e.g. FileBox). +//! This is a placeholder documenting the intended API; wiring is added later. + +use std::sync::Arc; + +#[allow(unused_imports)] +use crate::boxes::file::provider::FileIo; +#[allow(unused_imports)] +use crate::boxes::file::core_ro::CoreRoFileIo; + +#[allow(dead_code)] +pub enum FileBoxMode { Auto, CoreRo, PluginOnly } + +#[allow(dead_code)] +pub fn read_filebox_mode_from_env() -> FileBoxMode { + match std::env::var("NYASH_FILEBOX_MODE").unwrap_or_else(|_| "auto".to_string()).as_str() { + "core-ro" => FileBoxMode::CoreRo, + "plugin-only" => FileBoxMode::PluginOnly, + _ => { + if std::env::var("NYASH_DISABLE_PLUGINS").as_deref() == Ok("1") { + FileBoxMode::CoreRo + } else { FileBoxMode::Auto } + } + } +} + +#[allow(dead_code)] +pub fn select_file_provider(mode: FileBoxMode) -> Arc { + match mode { + FileBoxMode::CoreRo => Arc::new(CoreRoFileIo::new()), + FileBoxMode::PluginOnly | FileBoxMode::Auto => { + // TODO: if plugin present, return PluginFileIo; otherwise fallback/Fail-Fast + Arc::new(CoreRoFileIo::new()) + } + } +} + diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index b563eebf..30121a68 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -20,6 +20,18 @@ impl NyashRunner { // - Prefer plugin implementations for core boxes // - Optionally fail fast when plugins are missing (NYASH_VM_PLUGIN_STRICT=1) { + // FileBox provider initialization + use crate::runner::modes::common_util::provider_registry; + use nyash_rust::runtime::provider_lock; + + let filebox_mode = provider_registry::read_filebox_mode_from_env(); + let filebox_provider = provider_registry::select_file_provider(filebox_mode); + if let Err(e) = provider_lock::set_filebox_provider(filebox_provider) { + if !quiet_pipe { + eprintln!("[warn] FileBox provider already set: {}", e); + } + } + // Initialize unified registry globals (idempotent) nyash_rust::runtime::init_global_unified_registry(); diff --git a/src/runtime/provider_lock.rs b/src/runtime/provider_lock.rs index b9ff6f8e..5d630a38 100644 --- a/src/runtime/provider_lock.rs +++ b/src/runtime/provider_lock.rs @@ -6,10 +6,12 @@ */ use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; +use crate::boxes::file::provider::FileIo; static LOCKED: AtomicBool = AtomicBool::new(false); static WARN_ONCE: OnceLock<()> = OnceLock::new(); +static FILEBOX_PROVIDER: OnceLock> = OnceLock::new(); /// Return true when providers are locked pub fn is_locked() -> bool { LOCKED.load(Ordering::Relaxed) } @@ -36,3 +38,14 @@ pub fn guard_before_new_box(box_type: &str) -> Result<(), String> { Ok(()) } +/// Set the global FileBox provider (can only be called once) +pub fn set_filebox_provider(provider: Arc) -> Result<(), String> { + FILEBOX_PROVIDER.set(provider) + .map_err(|_| "FileBox provider already set".to_string()) +} + +/// Get the global FileBox provider +pub fn get_filebox_provider() -> Option<&'static Arc> { + FILEBOX_PROVIDER.get() +} +