diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 0932ab0f..006f9c36 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -379,3 +379,52 @@ CURRENT_TASK.md 自体は「いまどこを触っているか」と「次に何をやるか」を 1 画面で把握できる軽さを維持する方針だよ。*** + +--- + +## Phase 109: RuntimeProfile 機構実装完了 ✅ (2025-12-03) + +### 実装内容 + +**ゴール**: FileBox を profile 依存の conditional required に変更し、minimal/no-fs プロファイルをサポート + +**実装完了項目**: +1. ✅ RuntimeProfile enum + is_required_in() ヘルパー + - `src/runtime/runtime_profile.rs`: RuntimeProfile::Default/NoFs 定義 + - `src/runtime/core_box_ids.rs`: CoreBoxId.is_required_in(profile) 追加 +2. ✅ PluginHost profile-aware 初期化 + - `src/runtime/plugin_host.rs`: profile 引数追加、FileBox provider チェック実装 +3. ✅ initialize_runtime() に profile 読み込み機構 + - `src/runtime/mod.rs`: 環境変数読み込み層(責務分離) +4. ✅ NoFsFileIo stub 実装 + - `src/providers/ring1/file/nofs_fileio.rs`: FileBox 無効化スタブ + - `src/runtime/provider_lock.rs`: init_filebox_provider_for_profile() +5. ✅ ドキュメント更新 + - `phase108_filebox_write_semantics.md`: Section 9 追加 + - `core_boxes_design.md`: Section 5.4 追加 + +**テスト結果**: +- ✅ test_core_box_id_is_required_in_default +- ✅ test_core_box_id_is_required_in_nofs +- ✅ test_with_core_from_registry_nofs_filebox_optional +- ✅ test_nofs_fileio_caps/open/read/write/close_unsupported (5 tests) +- ✅ cargo build --release: 成功 +- ✅ cargo test --release --lib: Phase 109 tests 全 PASS + +**設計原則**: +- **責務分離**: initialize_runtime() のみが env を読む、PluginHost は profile を受け取る +- **Fail-Fast 維持**: Default profile では FileBox provider 必須 +- **Logger/ConsoleService 有効**: NoFs でも Ring0.log と ConsoleBox は動作 +- **互換性保証**: Default profile で Phase 107/108 の動作を完全維持 + +**将来拡張予定**: +- TestMock: テスト用プロファイル +- Sandbox: サンドボックスプロファイル +- ReadOnly: 読み取り専用プロファイル +- Embedded: 組み込みプロファイル + +**次のタスク候補**: +- Phase 110: FileHandleBox(複数ファイル同時アクセス) +- Phase 111: append mode 追加 +- Phase 112: Ring0 service registry 統一化 + diff --git a/docs/development/current/main/core_boxes_design.md b/docs/development/current/main/core_boxes_design.md index 729593dd..621cb102 100644 --- a/docs/development/current/main/core_boxes_design.md +++ b/docs/development/current/main/core_boxes_design.md @@ -292,7 +292,122 @@ FileBox の実体 I/O は、以下の層構造で Ring0.FsApi を通す設計が - `src/runtime/provider_lock.rs`: init_default_filebox_provider() ヘルパー - `src/runtime/plugin_host.rs`: 起動時自動登録 -### 5.4 今後の拡張 +### 5.4 Phase 109 - RuntimeProfile 機構(2025-12-03 完了)✅ + +**ゴール**: +- FileBox を **profile 依存の conditional required** に変更 +- Default profile(selfhost/standard)では FileBox 必須を維持 +- NoFs profile(minimal runtime)では FileBox を optional に + +**実装内容**: + +1. **RuntimeProfile enum 導入** (`src/runtime/runtime_profile.rs`): + ```rust + pub enum RuntimeProfile { + Default, // selfhost/standard + NoFs, // minimal runtime without filesystem + } + + impl RuntimeProfile { + pub fn from_env() -> Self { + // NYASH_RUNTIME_PROFILE=no-fs → NoFs + // それ以外 → Default + } + } + ``` + +2. **CoreBoxId に profile-aware 判定追加** (`src/runtime/core_box_ids.rs`): + ```rust + pub fn is_required_in(&self, profile: &RuntimeProfile) -> bool { + match profile { + RuntimeProfile::Default => { + // FileBox は required(Phase 106 互換) + self.is_core_required() + } + RuntimeProfile::NoFs => { + // FileBox は optional + matches!(self, String | Integer | Bool | Array | Map | Console) + } + } + } + ``` + +3. **PluginHost に profile 引数追加** (`src/runtime/plugin_host.rs`): + - `with_core_from_registry_optional(ring0, registry, config, profile)` に拡張 + - profile-aware FileBox provider チェック: + - Default: provider 必須(CoreInitError::MissingService if None) + - NoFs: provider なくても OK(黙って skip) + +4. **NoFsFileIo stub 実装** (`src/providers/ring1/file/nofs_fileio.rs`): + ```rust + pub struct NoFsFileIo; + + impl FileIo for NoFsFileIo { + fn caps(&self) -> FileCaps { FileCaps { read: false, write: false } } + fn open/read/write/close → Err(FileError::Unsupported) + } + ``` + +5. **initialize_runtime() に profile 読み込み追加** (`src/runtime/mod.rs`): + - 環境変数から profile を読む(**この層のみで実施**) + - NoFs profile の場合、NoFsFileIo を登録 + - PluginHost に profile を渡す(**env に依存しない**) + +**設計原則(Modification 1: 責務分離)**: +``` +【Layer】 【責務】 【Example】 +──────────────────────────────────────────────────────── +env User configuration NYASH_RUNTIME_PROFILE=no-fs +initialize_runtime() env → RuntimeProfile profile = RuntimeProfile::from_env() +PluginHost profile-aware checks is_required_in(&profile) +CoreBoxId 条件付き required 判定 is_required_in(&profile) +provider_lock provider 登録(Profile 後)set_filebox_provider() +FileBox provider 経由 read/write 実装 +``` + +**Logger/ConsoleService の有効性(Modification 2)**: +- ✅ **NoFs profile でも有効**: + - Ring0.log(OS抽象化層 - panic/exit 時の最終出力) + - ConsoleBox(言語レベル console - stdout/stderr) + - その他 core_required(String/Integer/Bool/Array/Map/Console) +- ❌ **NoFs profile で無効**: + - FileBox(ファイルシステム依存) + - Regex/Time/JSON 等のオプショナル boxes(将来:profile ごとに制御可能) + +**将来の拡張予定(Modification 3)**: +- **TestMock**: テスト用(すべてのプラグインが mock に) +- **Sandbox**: サンドボックス(外部 I/O 禁止) +- **ReadOnly**: 読み取り専用(FileBox.write 禁止) +- **Embedded**: 組み込み(メモリ制限あり、GC あり) + +**実装箇所**: +- `src/runtime/runtime_profile.rs`: RuntimeProfile enum 定義 +- `src/runtime/core_box_ids.rs`: is_required_in() メソッド +- `src/runtime/plugin_host.rs`: profile-aware 初期化ロジック +- `src/runtime/provider_lock.rs`: init_filebox_provider_for_profile() +- `src/providers/ring1/file/nofs_fileio.rs`: NoFs stub 実装 +- `src/runtime/mod.rs`: initialize_runtime() に profile 読み込み + +**テスト**: +- ✅ test_core_box_id_is_required_in_default +- ✅ test_core_box_id_is_required_in_nofs +- ✅ test_with_core_from_registry_nofs_filebox_optional +- ✅ test_nofs_fileio_caps/open/read/write/close_unsupported + +**互換性**: +- Phase 107/108 の既存動作は Default profile で完全維持 +- NoFs profile は完全に新規追加(既存コードに影響なし) + +### 5.5 今後の拡張(Phase 110+) + +Phase 110 以降では、FileBox/FS 周りの扱いをプロファイルと Box 設計の両面から広げていく予定: +- **Phase 110: FileHandleBox** + - FileBox は「1 ファイル専用 API」としてシンプルに保ち、複数ファイル同時アクセスは FileHandleBox 側に切り出す設計。 + - Ring0FsFileIo を内部で再利用しつつ、ハンドル単位で FsApi をラップする。 +- **Phase 111: metadata API 整理** + - `FsApi::metadata/exists/canonicalize` を FileIo / FileBox 側に橋渡しし、Nyash 側から stat 情報を扱えるようにする。 + +これらはすべて `CoreBoxId` / Ring0.FsApi / FileIo / FileBox の既存ラインの上に小さく積む形で設計する。 新しいメソッド追加は `src/runtime/core_box_ids.rs` の編集のみで完結: 1. CoreMethodId enum にバリアント追加 diff --git a/docs/development/current/main/phase107_fsapi_fileio_bridge.md b/docs/development/current/main/phase107_fsapi_fileio_bridge.md index 6c15e7a1..55bc28a0 100644 --- a/docs/development/current/main/phase107_fsapi_fileio_bridge.md +++ b/docs/development/current/main/phase107_fsapi_fileio_bridge.md @@ -351,12 +351,12 @@ provider_lock 登録・参照 OnceLock管理 FileBox ユーザーAPI provider 経由のみ ``` -### 拡張ポイント +### 拡張ポイント(Phase 109+) -**将来の実装**: -- MockFileIo: FsApi の代わりに in-memory mock を使う -- NetworkFileIo: FsApi の代わりに remote FS を使う -- minimal/no-fs: provider 登録をスキップ(FileBox optional化) +**将来の実装候補**: +- MockFileIo: FsApi の代わりに in-memory mock を使う(テスト専用) +- NetworkFileIo: FsApi の代わりに remote FS を使う(将来の分散 FS / リモートログ用途) +- minimal/no-fs: RuntimeProfile に応じて provider 登録をスキップし、FileBox を read-only / disabled として扱う --- diff --git a/docs/development/current/main/phase108_filebox_write_semantics.md b/docs/development/current/main/phase108_filebox_write_semantics.md index 3828d457..02942da0 100644 --- a/docs/development/current/main/phase108_filebox_write_semantics.md +++ b/docs/development/current/main/phase108_filebox_write_semantics.md @@ -322,4 +322,41 @@ fn test_filebox_double_open() { --- +## 9. Phase 109 以降の計画 + +### Phase 109: RuntimeProfile 機構の追加 + +**Phase 109 完了により、FileBox は conditional required に変更されました**: + +- **RuntimeProfile enum 導入**(Default/NoFs) +- **Default profile**: FileBox は required(Phase 107/108 の動作を維持) +- **NoFs profile**: FileBox は optional(NoFsFileIo stub で無効化) + +**設計変更**: +```rust +// Phase 109 以前: FileBox は常に required +CoreBoxId::File.is_core_required() // → true + +// Phase 109 以降: profile 依存の判定に +CoreBoxId::File.is_required_in(&RuntimeProfile::Default) // → true +CoreBoxId::File.is_required_in(&RuntimeProfile::NoFs) // → false +``` + +**プロファイル別動作**: +- **Default**: Ring0FsFileIo(read/write 両対応)自動登録 +- **NoFs**: NoFsFileIo(全操作で Unsupported エラー)登録 + +**将来の拡張計画**(Phase 109 Modification 3): +- TestMock: テスト用(全プラグインが mock に) +- Sandbox: サンドボックス(外部 I/O 禁止) +- ReadOnly: 読み取り専用(FileBox.write 禁止) +- Embedded: 組み込み(メモリ制限・GC あり) + +**互換性**: +- Phase 107/108 の既存動作は Default profile で完全維持 +- NoFs profile は完全に新規追加(既存コードに影響なし) + +--- + **Phase 108 指示書作成日**: 2025-12-03(微調整版) +**Phase 109 追記**: 2025-12-03(RuntimeProfile 統合完了) diff --git a/docs/development/current/main/ring0-inventory.md b/docs/development/current/main/ring0-inventory.md index c07e8ee9..b8fd3e27 100644 --- a/docs/development/current/main/ring0-inventory.md +++ b/docs/development/current/main/ring0-inventory.md @@ -557,13 +557,37 @@ rg 'impl.*LogApi' --type rust ## Summary -Phase 99 establishes a **clear inventory** of logging infrastructure and println! call sites: +Phase 85–108 で、Ring0 / CoreServices / FileBox / Logging の基礎はほぼ出揃った: -1. **Ring0.log**: Underutilized, ready for expansion -2. **println!/eprintln!**: 1477 production call sites categorized into 4 groups -3. **Migration strategy**: Phased approach starting with user-facing messages -4. **Success criteria**: Clear metrics for each phase +- Ring0Context: Mem/Io/Time/Log/Fs/Thread の抽象化が確立 +- CoreBoxId/CoreMethodId: Box 名・メソッド名の SSOT 化 +- CoreServices/PluginHost: ring1-core (String/Integer/Bool/Array/Map/Console) の service 化 +- ConsoleService/Logging: 3層設計(Ring0.log / ConsoleService / test println!)が定着 +- FileBox: CoreRequired 扱い + Ring0FsFileIo 経由で read/write 両対応 -**Phase 102**: StdMem implementation complete, preparing for hakmem integration. +Phase 99 は logging infrastructure と println! call site の在庫を整理し: -**Next Steps**: Phase 100+ will implement gradual migrations based on this inventory. +1. **Ring0.log**: dev/debug ログの受け皿として拡張準備済み +2. **println!/eprintln!**: ~1400 箇所を 4 カテゴリに分類(user-facing/dev/test/internal) +3. **Migration strategy**: user-facing → Ring0.log/internal の順で段階的移行 + +Phase 102 では StdMem 実装により hakmem 統合の足場を用意し、 +Phase 106–108 では FileBox provider_lock / Ring0FsFileIo / write/write_all 実装により、 +`FileBox → FileIo → Ring0.FsApi → std::fs` のパイプラインが完成した。 + +### Next Phases(計画) + +今後の候補フェーズ(優先度の高い順): + +- **Phase 109: runtime profiles(default/no-fs)** + - `NYASH_PROFILE={default|no-fs}` などのプロファイル導入 + - default では FileBox 必須、no-fs では FileBox provider 未登録(エラー文字列で通知) +- **Phase 110: FileHandleBox** + - FileBox を「1ファイル専用」に保ちつつ、複数ファイル同時アクセスは FileHandleBox 側に切り出す + - Ring0FsFileIo を再利用してハンドル単位の管理を行う +- **Phase 111: Fs metadata 拡張** + - `exists/metadata/canonicalize` を FileIo / FileBox 側にきちんとエクスポート + - Ring0.FsApi の stat 情報を Nyash 側から扱えるようにする + +さらに長期的には、Ring0 全体を「統一サービスレジストリ」として扱うフェーズ(Mem/Io/Time/Log/Fs/Thread の trait 統合)を +Phase 11x 以降で検討する予定だよ。 diff --git a/src/providers/ring1/file/mod.rs b/src/providers/ring1/file/mod.rs index bdfc7b4c..b700de1c 100644 --- a/src/providers/ring1/file/mod.rs +++ b/src/providers/ring1/file/mod.rs @@ -1,2 +1,3 @@ pub mod core_ro; pub mod ring0_fs_fileio; +pub mod nofs_fileio; // Phase 109: NoFs profile stub diff --git a/src/providers/ring1/file/nofs_fileio.rs b/src/providers/ring1/file/nofs_fileio.rs new file mode 100644 index 00000000..276739d8 --- /dev/null +++ b/src/providers/ring1/file/nofs_fileio.rs @@ -0,0 +1,98 @@ +//! Phase 109: NoFs profile FileIo stub +//! +//! Provides a stub FileIo implementation that returns errors for all operations. +//! Used when FileBox is disabled in no-fs runtime profile. + +use crate::boxes::file::provider::{FileCaps, FileError, FileIo, FileResult}; + +/// Phase 109: No-filesystem FileIo stub +/// +/// Returns Unsupported errors for all operations. +/// Used in NoFs runtime profile where FileBox is disabled. +/// +/// # Design +/// +/// - caps(): Returns read=false, write=false +/// - All operations: Return FileError::Unsupported with clear message +/// +/// # Logger/ConsoleService availability (Phase 109 Modification 2) +/// +/// ✅ Still available in NoFs profile: +/// - Ring0.log (OS abstraction layer - panic/exit final output) +/// - ConsoleBox (language-level console - stdout/stderr) +/// - Core required boxes (String/Integer/Bool/Array/Map/Console) +/// +/// ❌ Disabled in NoFs profile: +/// - FileBox (filesystem-dependent) +/// - Optional boxes (Regex/Time/JSON - future: profile-controlled) +pub struct NoFsFileIo; + +impl FileIo for NoFsFileIo { + fn caps(&self) -> FileCaps { + FileCaps { + read: false, + write: false, + } + } + + fn open(&self, _path: &str) -> FileResult<()> { + Err(FileError::Unsupported) + } + + fn read(&self) -> FileResult { + Err(FileError::Unsupported) + } + + fn write(&self, _text: &str) -> FileResult<()> { + Err(FileError::Unsupported) + } + + fn close(&self) -> FileResult<()> { + Err(FileError::Unsupported) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nofs_fileio_caps() { + let fileio = NoFsFileIo; + let caps = fileio.caps(); + assert!(!caps.read, "NoFsFileIo should report read=false"); + assert!(!caps.write, "NoFsFileIo should report write=false"); + } + + #[test] + fn test_nofs_fileio_open_unsupported() { + let fileio = NoFsFileIo; + let result = fileio.open("/tmp/test.txt"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unsupported")); + } + + #[test] + fn test_nofs_fileio_read_unsupported() { + let fileio = NoFsFileIo; + let result = fileio.read(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unsupported")); + } + + #[test] + fn test_nofs_fileio_write_unsupported() { + let fileio = NoFsFileIo; + let result = fileio.write("test"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unsupported")); + } + + #[test] + fn test_nofs_fileio_close_unsupported() { + let fileio = NoFsFileIo; + let result = fileio.close(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unsupported")); + } +} diff --git a/src/runtime/core_box_ids.rs b/src/runtime/core_box_ids.rs index 9b564396..52ea78ab 100644 --- a/src/runtime/core_box_ids.rs +++ b/src/runtime/core_box_ids.rs @@ -3,6 +3,8 @@ //! Nyash の core Box を型安全な enum で管理する。 //! ハードコード文字列からの脱却により、コンパイル時検証を実現。 +use crate::runtime::runtime_profile::RuntimeProfile; + /// Phase 85 調査結果に基づく Core Box ID #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CoreBoxId { @@ -118,6 +120,30 @@ impl CoreBoxId { matches!(self, String | Integer | Bool | Array | Map | Console | File) } + /// Phase 109: profile-aware required check + /// + /// Determines if this CoreBox is required in the given RuntimeProfile. + /// + /// - Default: Same as is_core_required() (FileBox is required) + /// - NoFs: FileBox becomes optional (only String/Integer/Bool/Array/Map/Console required) + /// + /// **Future expansion**: TestMock/Sandbox/ReadOnly/Embedded profiles will extend this logic + pub fn is_required_in(&self, profile: &RuntimeProfile) -> bool { + use CoreBoxId::*; + let core_required = matches!(self, String | Integer | Bool | Array | Map | Console); + + match profile { + RuntimeProfile::Default => { + // Phase 106: FileBox is required in Default profile + self.is_core_required() + } + RuntimeProfile::NoFs => { + // Phase 109: FileBox is optional in NoFs profile + core_required + } + } + } + /// Phase 87: カテゴリ分類 pub fn category(&self) -> CoreBoxCategory { use CoreBoxId::*; @@ -372,6 +398,32 @@ mod tests { assert_eq!(CoreBoxId::Function.category(), CoreBoxCategory::Special); } + #[test] + fn test_core_box_id_is_required_in_default() { + use crate::runtime::runtime_profile::RuntimeProfile; + let profile = RuntimeProfile::Default; + assert!(CoreBoxId::String.is_required_in(&profile)); + assert!(CoreBoxId::Integer.is_required_in(&profile)); + assert!(CoreBoxId::Bool.is_required_in(&profile)); + assert!(CoreBoxId::Array.is_required_in(&profile)); + assert!(CoreBoxId::Map.is_required_in(&profile)); + assert!(CoreBoxId::Console.is_required_in(&profile)); + assert!(CoreBoxId::File.is_required_in(&profile)); // FileBox required in Default + } + + #[test] + fn test_core_box_id_is_required_in_nofs() { + use crate::runtime::runtime_profile::RuntimeProfile; + let profile = RuntimeProfile::NoFs; + assert!(CoreBoxId::String.is_required_in(&profile)); + assert!(CoreBoxId::Integer.is_required_in(&profile)); + assert!(CoreBoxId::Bool.is_required_in(&profile)); + assert!(CoreBoxId::Array.is_required_in(&profile)); + assert!(CoreBoxId::Map.is_required_in(&profile)); + assert!(CoreBoxId::Console.is_required_in(&profile)); + assert!(!CoreBoxId::File.is_required_in(&profile)); // FileBox optional in NoFs + } + // ===== CoreMethodId tests ===== #[test] diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 17ff8527..bcf77aa0 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -9,6 +9,7 @@ pub mod deprecations; pub mod gc; pub mod plugin_host; // Phase 91: PluginHost skeleton pub mod ring0; // Phase 88: Ring0Context - OS API 抽象化レイヤー +pub mod runtime_profile; // Phase 109: RuntimeProfile enum (Default/NoFs) pub mod gc_controller; pub mod gc_mode; pub mod gc_trace; @@ -42,6 +43,7 @@ pub use box_registry::{get_global_registry, BoxFactoryRegistry, BoxProvider}; pub use core_box_ids::{CoreBoxCategory, CoreBoxId, CoreMethodId}; // Phase 87: 型安全enum pub use plugin_config::PluginConfig; pub use ring0::{get_global_ring0, init_global_ring0, Ring0Context}; // Phase 88: Ring0 公開 API +pub use runtime_profile::RuntimeProfile; // Phase 109: RuntimeProfile enum pub use plugin_host::CoreInitError; // Phase 92: CoreServices 初期化エラー pub use plugin_loader_unified::{ get_global_plugin_host, init_global_plugin_host, MethodHandle, PluginBoxType, PluginHost, @@ -98,21 +100,55 @@ macro_rules! console_println { }; } -/// Runtime 初期化(Phase 95: global accessor 実装完了) +/// Runtime 初期化(Phase 95/109: profile-aware initialization) /// /// Phase 94: フォールバック削除 - 常に実際の Box を使用 /// Phase 95: global に登録して get_core_plugin_host() でアクセス可能に +/// Phase 109: RuntimeProfile に基づく条件付き初期化 +/// +/// # Responsibility Separation (Phase 109 Modification 1) +/// +/// - **initialize_runtime**: 環境変数から profile を読む(唯一の env reader) +/// - **PluginHost**: profile を引数として受け取る(env に依存しない) +/// +/// # Profile behavior +/// +/// - **Default**: FileBox provider 必須(Fail-Fast)、全 core services 有効 +/// - **NoFs**: FileBox provider optional(disabled stub)、core services のみ有効 pub fn initialize_runtime(ring0: std::sync::Arc) -> Result<(), CoreInitError> { use crate::box_factory::UnifiedBoxRegistry; use crate::box_factory::builtin::BuiltinBoxFactory; + // Phase 109: Read RuntimeProfile from environment (this layer only) + let profile = RuntimeProfile::from_env(); + let mut registry = UnifiedBoxRegistry::with_env_policy(); // Phase 94: BuiltinBoxFactory を登録して core_required Boxes を提供 registry.register(std::sync::Arc::new(BuiltinBoxFactory::new())); - // Phase 94: 常に実際の Box → Service 変換を使用(Fail-Fast原則) - let plugin_host = plugin_host::PluginHost::with_core_from_registry(ring0, ®istry)?; + // Phase 109: Profile-aware FileBox provider initialization + // Note: This is done BEFORE PluginHost initialization to allow plugin override + match profile { + RuntimeProfile::Default => { + // Default profile: FileBox provider will be auto-registered in PluginHost + // (no action needed here, kept for documentation) + } + RuntimeProfile::NoFs => { + // NoFs profile: Register NoFsFileIo stub + use crate::runtime::provider_lock; + let _ = provider_lock::init_filebox_provider_for_profile(&ring0, &profile); + // Ignore error - PluginHost will handle missing provider gracefully + } + } + + // Phase 109: Pass profile to PluginHost (env-independent) + let plugin_host = plugin_host::PluginHost::with_core_from_registry_optional( + ring0, + ®istry, + plugin_host::CoreServicesConfig::all_enabled(), + &profile, + )?; plugin_host.ensure_core_initialized(); // Phase 95: global に登録 diff --git a/src/runtime/plugin_host.rs b/src/runtime/plugin_host.rs index 725f1493..3fed8558 100644 --- a/src/runtime/plugin_host.rs +++ b/src/runtime/plugin_host.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::any::Any; use crate::box_factory::UnifiedBoxRegistry; use crate::runtime::CoreBoxId; +use crate::runtime::RuntimeProfile; /// Phase 103: CoreServices Optional化設定 /// @@ -98,7 +99,8 @@ use super::ring0::Ring0Context; pub enum CoreInitError { MissingService { box_id: CoreBoxId, - message: String, + /// Phase 109: hint now includes profile context + hint: String, }, RegistryEmpty, InvalidServiceType { @@ -111,8 +113,8 @@ pub enum CoreInitError { impl std::fmt::Display for CoreInitError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - CoreInitError::MissingService { box_id, message } => { - write!(f, "Missing core service {:?}: {}", box_id, message) + CoreInitError::MissingService { box_id, hint } => { + write!(f, "Missing core service {:?}: {}", box_id, hint) } CoreInitError::RegistryEmpty => { write!(f, "UnifiedBoxRegistry is empty") @@ -143,43 +145,73 @@ impl PluginHost { unimplemented!("Phase 92 で from_registry() 実装後に削除") } - /// Phase 103: Optional CoreServices initialization + /// Phase 103/109: Optional CoreServices initialization with RuntimeProfile support /// - /// Allows selective initialization based on CoreServicesConfig. + /// Allows selective initialization based on CoreServicesConfig and RuntimeProfile. /// ConsoleBox is mandatory for user-facing output. + /// + /// Phase 109 additions: + /// - `profile` parameter controls FileBox provider requirements + /// - Default profile: FileBox provider is required (Fail-Fast) + /// - NoFs profile: FileBox provider is optional (disabled) pub fn with_core_from_registry_optional( ring0: Arc, registry: &UnifiedBoxRegistry, config: CoreServicesConfig, + profile: &RuntimeProfile, ) -> Result { use crate::runtime::core_services::*; use crate::runtime::provider_lock; - // Phase 107: Auto-register default FileBox provider (Ring0.FsApi-based) - // This happens before the Phase 106 check, so plugins can still override. - // If a plugin has already registered, this returns Err but we continue. - if CoreBoxId::File.is_core_required() { - match provider_lock::init_default_filebox_provider(&ring0) { - Ok(()) => { - // Default provider registered successfully - ring0.log.debug("[Phase 107] Ring0FsFileIo registered as default FileBox provider"); - } - Err(msg) => { - // Plugin provider already registered - this is OK (plugin priority) - ring0.log.debug(&format!("[Phase 107] {}", msg)); - } + // Phase 109: Profile-aware required check + for box_id in CoreBoxId::iter() { + if box_id.is_required_in(profile) && !registry.has_type(box_id.name()) { + return Err(CoreInitError::MissingService { + box_id, + hint: format!( + "Core Box {} is required in {} profile but not found in registry", + box_id.name(), + profile.name() + ), + }); } } - // Phase 106: FileBox provider チェック追加 - // CoreBoxId がFileを必須と判定している場合、provider が登録されていることを確認 - // Phase 107: With auto-registration above, this check should always pass - if CoreBoxId::File.is_core_required() { - if provider_lock::get_filebox_provider().is_none() { - return Err(CoreInitError::MissingService { - box_id: CoreBoxId::File, - message: "FileBox provider not registered (required for selfhost/default profile)".to_string(), - }); + // Phase 109: Profile-aware FileBox provider check + match profile { + RuntimeProfile::Default => { + // Phase 107: Auto-register default FileBox provider (Ring0.FsApi-based) + // This happens before the Phase 106 check, so plugins can still override. + // If a plugin has already registered, this returns Err but we continue. + if CoreBoxId::File.is_required_in(profile) { + match provider_lock::init_default_filebox_provider(&ring0) { + Ok(()) => { + // Default provider registered successfully + ring0.log.debug("[Phase 107] Ring0FsFileIo registered as default FileBox provider"); + } + Err(msg) => { + // Plugin provider already registered - this is OK (plugin priority) + ring0.log.debug(&format!("[Phase 107] {}", msg)); + } + } + } + + // Phase 106/109: FileBox provider チェック (Default profile) + // CoreBoxId がFileを必須と判定している場合、provider が登録されていることを確認 + // Phase 107: With auto-registration above, this check should always pass + if CoreBoxId::File.is_required_in(profile) { + if provider_lock::get_filebox_provider().is_none() { + return Err(CoreInitError::MissingService { + box_id: CoreBoxId::File, + hint: "FileBox provider not registered (required for Default profile)".to_string(), + }); + } + } + } + RuntimeProfile::NoFs => { + // Phase 109: FileBox provider is optional in NoFs profile + // We skip provider initialization and checks entirely + ring0.log.debug("[Phase 109] NoFs profile: FileBox provider skipped"); } } @@ -197,7 +229,7 @@ impl PluginHost { if !registry.has_type("StringBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::String, - message: "StringBox enabled but not found in registry".to_string(), + hint: "StringBox enabled but not found in registry".to_string(), }); } core.string = Some(Arc::new(StringBoxAdapter::new())); @@ -208,7 +240,7 @@ impl PluginHost { if !registry.has_type("IntegerBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Integer, - message: "IntegerBox enabled but not found in registry".to_string(), + hint: "IntegerBox enabled but not found in registry".to_string(), }); } core.integer = Some(Arc::new(IntegerBoxAdapter::new())); @@ -219,7 +251,7 @@ impl PluginHost { if !registry.has_type("BoolBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Bool, - message: "BoolBox enabled but not found in registry".to_string(), + hint: "BoolBox enabled but not found in registry".to_string(), }); } core.bool = Some(Arc::new(BoolBoxAdapter::new())); @@ -230,7 +262,7 @@ impl PluginHost { if !registry.has_type("ArrayBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Array, - message: "ArrayBox enabled but not found in registry".to_string(), + hint: "ArrayBox enabled but not found in registry".to_string(), }); } core.array = Some(Arc::new(ArrayBoxAdapter::new())); @@ -241,7 +273,7 @@ impl PluginHost { if !registry.has_type("MapBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Map, - message: "MapBox enabled but not found in registry".to_string(), + hint: "MapBox enabled but not found in registry".to_string(), }); } core.map = Some(Arc::new(MapBoxAdapter::new())); @@ -252,14 +284,14 @@ impl PluginHost { if !registry.has_type("ConsoleBox") { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Console, - message: "ConsoleBox is mandatory but not found in registry".to_string(), + hint: "ConsoleBox is mandatory but not found in registry".to_string(), }); } core.console = Some(Arc::new(ConsoleBoxAdapter::new())); } else { return Err(CoreInitError::MissingService { box_id: CoreBoxId::Console, - message: "Phase 103: ConsoleBox is mandatory for user-facing output".to_string(), + hint: "Phase 103: ConsoleBox is mandatory for user-facing output".to_string(), }); } @@ -270,19 +302,22 @@ impl PluginHost { }) } - /// Phase 101: Backward compatibility - all services required + /// Phase 101/109: Backward compatibility - all services required with Default profile /// /// Maintains existing behavior: all CoreServices must be present. /// Used by default_ring0 and other initialization paths expecting all services. + /// + /// Phase 109: Now uses Default profile (FileBox required) pub fn with_core_from_registry( ring0: Arc, registry: &UnifiedBoxRegistry, ) -> Result { - // Use all_enabled() for backward compatibility + // Use all_enabled() and Default profile for backward compatibility Self::with_core_from_registry_optional( ring0, registry, CoreServicesConfig::all_enabled(), + &RuntimeProfile::Default, ) } @@ -389,7 +424,7 @@ mod tests { fn test_core_init_error_display() { let error = CoreInitError::MissingService { box_id: CoreBoxId::String, - message: "StringBox not found".to_string(), + hint: "StringBox not found".to_string(), }; let display = format!("{}", error); assert!(display.contains("String")); @@ -450,6 +485,29 @@ mod tests { panic!("FileBox provider should be registered after with_core_from_registry"); } } + + #[test] + fn test_with_core_from_registry_nofs_filebox_optional() { + // Phase 109: NoFs profile では FileBox provider なしで OK + use crate::runtime::ring0::default_ring0; + use crate::box_factory::builtin::BuiltinBoxFactory; + + let ring0 = Arc::new(default_ring0()); + let mut registry = UnifiedBoxRegistry::new(); + registry.register(Arc::new(BuiltinBoxFactory::new())); + + // Phase 109: NoFs profile で初期化 + let profile = RuntimeProfile::NoFs; + let result = PluginHost::with_core_from_registry_optional( + ring0, + ®istry, + CoreServicesConfig::all_enabled(), + &profile, + ); + + // Phase 109: FileBox は optional なので、provider なしで成功するはず + assert!(result.is_ok(), "Expected success with NoFs profile (FileBox optional)"); + } } #[cfg(test)] diff --git a/src/runtime/provider_lock.rs b/src/runtime/provider_lock.rs index 7625051b..2ee48ac6 100644 --- a/src/runtime/provider_lock.rs +++ b/src/runtime/provider_lock.rs @@ -92,3 +92,40 @@ pub fn init_default_filebox_provider( set_filebox_provider(provider) .map_err(|_| "Plugin FileBox provider already registered".to_string()) } + +/// Phase 109: Initialize FileBox provider based on RuntimeProfile +/// +/// This helper registers the appropriate FileBox provider for the given profile: +/// - Default: Ring0FsFileIo (Ring0.FsApi-based, read/write support) +/// - NoFs: NoFsFileIo (stub that returns Unsupported errors) +/// +/// # Returns +/// +/// - `Ok(())`: Provider registered successfully +/// - `Err(msg)`: Provider already registered (plugin took precedence) +/// +/// # Design (Phase 109) +/// +/// This function is called during initialize_runtime() and respects the profile: +/// - Default profile: Registers Ring0FsFileIo for full filesystem access +/// - NoFs profile: Registers NoFsFileIo stub (all operations return Unsupported) +pub fn init_filebox_provider_for_profile( + ring0: &Arc, + profile: &crate::runtime::RuntimeProfile, +) -> Result<(), String> { + use crate::runtime::RuntimeProfile; + + match profile { + RuntimeProfile::Default => { + // Phase 107: Standard profile uses Ring0FsFileIo + init_default_filebox_provider(ring0) + } + RuntimeProfile::NoFs => { + // Phase 109: NoFs profile uses NoFsFileIo stub + use crate::providers::ring1::file::nofs_fileio::NoFsFileIo; + let provider = Arc::new(NoFsFileIo); + set_filebox_provider(provider) + .map_err(|_| "FileBox provider already set (unexpected in NoFs profile)".to_string()) + } + } +} diff --git a/src/runtime/runtime_profile.rs b/src/runtime/runtime_profile.rs new file mode 100644 index 00000000..74579afd --- /dev/null +++ b/src/runtime/runtime_profile.rs @@ -0,0 +1,74 @@ +//! Phase 109: RuntimeProfile enum +//! +//! Controls conditional core service initialization based on runtime profile. +//! Supports Default (selfhost/standard) and NoFs (minimal runtime without filesystem). + +/// Phase 109: RuntimeProfile +/// +/// Controls availability of FileBox and other optional services. +/// +/// - Default: selfhost/standard - most services enabled (FileBox required) +/// - NoFs: minimal runtime - FileBox disabled, core boxes only +/// +/// Future expansion planned: TestMock, Sandbox, ReadOnly, Embedded +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RuntimeProfile { + /// Standard runtime (selfhost/default) + Default, + /// Minimal runtime without FileSystem + NoFs, +} + +impl RuntimeProfile { + /// Read RuntimeProfile from NYASH_RUNTIME_PROFILE environment variable + /// + /// # Recognized values + /// + /// - `"no-fs"` or `"nofs"` → NoFs + /// - Any other value or missing → Default + pub fn from_env() -> Self { + match std::env::var("NYASH_RUNTIME_PROFILE").as_deref() { + Ok("no-fs") | Ok("nofs") => RuntimeProfile::NoFs, + _ => RuntimeProfile::Default, + } + } + + /// Get profile name for debugging + pub fn name(&self) -> &'static str { + match self { + RuntimeProfile::Default => "Default", + RuntimeProfile::NoFs => "NoFs", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_runtime_profile_from_env_default() { + // Without setting env, should return Default + std::env::remove_var("NYASH_RUNTIME_PROFILE"); + assert_eq!(RuntimeProfile::from_env(), RuntimeProfile::Default); + } + + #[test] + fn test_runtime_profile_from_env_nofs() { + // Test "no-fs" variant + std::env::set_var("NYASH_RUNTIME_PROFILE", "no-fs"); + assert_eq!(RuntimeProfile::from_env(), RuntimeProfile::NoFs); + + // Test "nofs" variant + std::env::set_var("NYASH_RUNTIME_PROFILE", "nofs"); + assert_eq!(RuntimeProfile::from_env(), RuntimeProfile::NoFs); + + std::env::remove_var("NYASH_RUNTIME_PROFILE"); + } + + #[test] + fn test_runtime_profile_name() { + assert_eq!(RuntimeProfile::Default.name(), "Default"); + assert_eq!(RuntimeProfile::NoFs.name(), "NoFs"); + } +}