feat(phase109): RuntimeProfile設計で FileBox を条件付き optional に

Phase 109 完全実装完了:
- RuntimeProfile enum (Default, NoFs) で profile 制御
- CoreBoxId.is_required_in(profile) で条件付き required/optional
- initialize_runtime() で env 読み込み責務を一元化(修正1)
- NoFsFileIo スタブで no-fs プロファイルでの FileBox 無効化(修正2)
- Logger/ConsoleService は no-fs でも有効と明示(修正2)
- Profile 拡張予定(TestMock/Sandbox/ReadOnly/Embedded)を予約(修正3)

実装ファイル:
- src/runtime/runtime_profile.rs (新規)
- src/providers/ring1/file/nofs_fileio.rs (新規)
- src/runtime/core_box_ids.rs (修正)
- src/runtime/plugin_host.rs (修正)
- src/runtime/provider_lock.rs (修正)
- docs 更新

テスト: Phase 109 11/11 PASS 
ビルド: cargo build --release SUCCESS 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-03 19:37:32 +09:00
parent 2eda4bc86b
commit 4ef3e7f56c
12 changed files with 634 additions and 53 deletions

View File

@ -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 統一化

View File

@ -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 profileselfhost/standardでは FileBox 必須を維持
- NoFs profileminimal 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 は requiredPhase 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.logOS抽象化層 - panic/exit 時の最終出力)
- ConsoleBox言語レベル console - stdout/stderr
- その他 core_requiredString/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 にバリアント追加

View File

@ -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 として扱う
---

View File

@ -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 は requiredPhase 107/108 の動作を維持)
- **NoFs profile**: FileBox は optionalNoFsFileIo 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**: Ring0FsFileIoread/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-03RuntimeProfile 統合完了)

View File

@ -557,13 +557,37 @@ rg 'impl.*LogApi' --type rust
## Summary
Phase 99 establishes a **clear inventory** of logging infrastructure and println! call sites:
Phase 85108 で、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 106108 では FileBox provider_lock / Ring0FsFileIo / write/write_all 実装により、
`FileBox → FileIo → Ring0.FsApi → std::fs` のパイプラインが完成した。
### Next Phases計画
今後の候補フェーズ(優先度の高い順):
- **Phase 109: runtime profilesdefault/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 以降で検討する予定だよ。

View File

@ -1,2 +1,3 @@
pub mod core_ro;
pub mod ring0_fs_fileio;
pub mod nofs_fileio; // Phase 109: NoFs profile stub

View File

@ -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<String> {
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"));
}
}

View File

@ -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]

View File

@ -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 optionaldisabled stub、core services のみ有効
pub fn initialize_runtime(ring0: std::sync::Arc<Ring0Context>) -> 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, &registry)?;
// 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,
&registry,
plugin_host::CoreServicesConfig::all_enabled(),
&profile,
)?;
plugin_host.ensure_core_initialized();
// Phase 95: global に登録

View File

@ -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<Ring0Context>,
registry: &UnifiedBoxRegistry,
config: CoreServicesConfig,
profile: &RuntimeProfile,
) -> Result<Self, CoreInitError> {
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<Ring0Context>,
registry: &UnifiedBoxRegistry,
) -> Result<Self, CoreInitError> {
// 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,
&registry,
CoreServicesConfig::all_enabled(),
&profile,
);
// Phase 109: FileBox は optional なので、provider なしで成功するはず
assert!(result.is_ok(), "Expected success with NoFs profile (FileBox optional)");
}
}
#[cfg(test)]

View File

@ -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<crate::runtime::ring0::Ring0Context>,
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())
}
}
}

View File

@ -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");
}
}