feat(llvm): Phase 97 Box/Policy refactoring complete

Box化完了:
- CallRoutePolicyBox: Call routing SSoT
- PrintArgMarshallerBox: Print marshalling SSoT
- TypeFactsBox: Type propagation SSoT
- PhiSnapshotPolicyBox: PHI contract SSoT
- PluginErrorContext: Structured error reporting

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-17 04:14:26 +09:00
parent 65763c1ed6
commit 6d73fc3404
16 changed files with 1861 additions and 64 deletions

View File

@ -0,0 +1,181 @@
/// Phase 97 Refactoring: Structured Error Reporter Box for Plugin Loader
///
/// This module provides structured error reporting with clear context,
/// attempted paths, and actionable hints for plugin loading failures.
use crate::bid::BidError;
use std::path::PathBuf;
/// Structured plugin error information
#[derive(Debug, Clone)]
pub struct PluginErrorContext {
pub kind: PluginErrorKind,
pub plugin_name: String,
pub message: String,
pub attempted_paths: Vec<String>,
pub hint: Option<String>,
}
/// Plugin error kind classification
#[derive(Debug, Clone, PartialEq)]
pub enum PluginErrorKind {
/// Plugin library file not found
MissingLibrary,
/// dlopen() failed
LoadFailed,
/// Plugin initialization failed
InitFailed,
/// Version mismatch
VersionMismatch,
}
impl PluginErrorContext {
/// Create error context for missing plugin
pub fn missing_library(
plugin_name: &str,
configured_path: &str,
attempted_paths: Vec<PathBuf>,
) -> Self {
let paths_str: Vec<String> = attempted_paths
.iter()
.map(|p| p.display().to_string())
.collect();
Self {
kind: PluginErrorKind::MissingLibrary,
plugin_name: plugin_name.to_string(),
message: format!(
"Plugin '{}' not found at configured path: {}",
plugin_name, configured_path
),
attempted_paths: paths_str,
hint: Some(
"Check LD_LIBRARY_PATH or configure nyash.toml [libraries] section"
.to_string(),
),
}
}
/// Create error context for load failure
pub fn load_failed(
plugin_name: &str,
path: &str,
error_msg: &str,
) -> Self {
Self {
kind: PluginErrorKind::LoadFailed,
plugin_name: plugin_name.to_string(),
message: format!(
"Failed to load plugin '{}' from {}: {}",
plugin_name, path, error_msg
),
attempted_paths: vec![path.to_string()],
hint: Some("Check plugin architecture (32/64-bit) and dependencies".to_string()),
}
}
/// Create error context for init failure
pub fn init_failed(plugin_name: &str, error_msg: &str) -> Self {
Self {
kind: PluginErrorKind::InitFailed,
plugin_name: plugin_name.to_string(),
message: format!(
"Plugin '{}' initialization failed: {}",
plugin_name, error_msg
),
attempted_paths: vec![],
hint: Some("Check plugin logs for initialization errors".to_string()),
}
}
/// Log structured error using global ring0 logger
pub fn log_structured(&self) {
use crate::runtime::get_global_ring0;
let ring0 = get_global_ring0();
match self.kind {
PluginErrorKind::MissingLibrary => {
ring0.log.error(&format!("[plugin/missing] {}", self.message));
if !self.attempted_paths.is_empty() {
ring0.log.error(&format!(
"[plugin/missing] Attempted paths: {}",
self.attempted_paths.join(", ")
));
}
if let Some(ref hint) = self.hint {
ring0.log.warn(&format!("[plugin/hint] {}", hint));
}
}
PluginErrorKind::LoadFailed => {
ring0.log.error(&format!("[plugin/init] {}", self.message));
if let Some(ref hint) = self.hint {
ring0.log.warn(&format!("[plugin/hint] {}", hint));
}
}
PluginErrorKind::InitFailed => {
ring0.log.error(&format!("[plugin/init] {}", self.message));
if let Some(ref hint) = self.hint {
ring0.log.warn(&format!("[plugin/hint] {}", hint));
}
}
PluginErrorKind::VersionMismatch => {
ring0.log.error(&format!("[plugin/version] {}", self.message));
if let Some(ref hint) = self.hint {
ring0.log.warn(&format!("[plugin/hint] {}", hint));
}
}
}
}
/// Convert to BidError
pub fn to_bid_error(&self) -> BidError {
match self.kind {
PluginErrorKind::MissingLibrary => BidError::PluginError,
PluginErrorKind::LoadFailed => BidError::PluginError,
PluginErrorKind::InitFailed => BidError::PluginError,
PluginErrorKind::VersionMismatch => BidError::VersionMismatch,
}
}
}
/// Helper function for logging and returning error
pub fn report_and_fail(ctx: PluginErrorContext) -> BidError {
ctx.log_structured();
ctx.to_bid_error()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_missing_library_context() {
let ctx = PluginErrorContext::missing_library(
"test_plugin",
"/usr/lib/test.so",
vec![
PathBuf::from("/usr/lib/test.so"),
PathBuf::from("/usr/lib/libtest.so"),
],
);
assert_eq!(ctx.kind, PluginErrorKind::MissingLibrary);
assert_eq!(ctx.plugin_name, "test_plugin");
assert_eq!(ctx.attempted_paths.len(), 2);
assert!(ctx.hint.is_some());
}
#[test]
fn test_load_failed_context() {
let ctx = PluginErrorContext::load_failed(
"test_plugin",
"/usr/lib/test.so",
"undefined symbol: foo",
);
assert_eq!(ctx.kind, PluginErrorKind::LoadFailed);
assert!(ctx.message.contains("undefined symbol"));
assert_eq!(ctx.attempted_paths.len(), 1);
}
}

View File

@ -1,3 +1,4 @@
use super::error_reporter::{report_and_fail, PluginErrorContext};
use super::specs;
use super::util::dbg_on;
use super::PluginLoaderV2;
@ -96,19 +97,13 @@ pub(super) fn load_plugin(
let lib_path = match lib_path {
Some(path) => path,
None => {
let mut attempted = candidates
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>();
attempted.sort();
attempted.dedup();
get_global_ring0().log.error(&format!(
"[plugin/missing] {}: no existing file for configured path='{}' (attempted={})",
// Phase 97: Use structured error reporter
let ctx = PluginErrorContext::missing_library(
lib_name,
base.display(),
attempted.join(", ")
));
return Err(BidError::PluginError);
&base.display().to_string(),
candidates,
);
return Err(report_and_fail(ctx));
}
};
if dbg_on() {
@ -119,13 +114,13 @@ pub(super) fn load_plugin(
));
}
let lib = unsafe { Library::new(&lib_path) }.map_err(|e| {
get_global_ring0().log.error(&format!(
"[plugin/init] dlopen failed for {} ({}): {}",
// Phase 97: Use structured error reporter
let ctx = PluginErrorContext::load_failed(
lib_name,
lib_path.display(),
e
));
BidError::PluginError
&lib_path.display().to_string(),
&e.to_string(),
);
report_and_fail(ctx)
})?;
let lib_arc = Arc::new(lib);
@ -197,3 +192,80 @@ fn candidate_paths(base: &Path) -> Vec<PathBuf> {
}
candidates
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::nyash_toml_v2::{NyashConfigV2, PluginPaths};
use std::env;
struct EnvGuard {
key: &'static str,
original: Option<String>,
}
impl EnvGuard {
fn set(key: &'static str, value: &str) -> Self {
let original = env::var(key).ok();
env::set_var(key, value);
Self { key, original }
}
fn unset(key: &'static str) -> Self {
let original = env::var(key).ok();
env::remove_var(key);
Self { key, original }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
if let Some(val) = &self.original {
env::set_var(self.key, val);
} else {
env::remove_var(self.key);
}
}
}
fn loader_with_missing_library(path: &str) -> PluginLoaderV2 {
let mut libraries = HashMap::new();
libraries.insert(
"missing_lib".to_string(),
LibraryDefinition {
boxes: vec!["FileBox".to_string()],
path: path.to_string(),
},
);
PluginLoaderV2 {
config: Some(NyashConfigV2 {
libraries,
plugin_paths: PluginPaths::default(),
plugins: HashMap::new(),
box_types: HashMap::new(),
}),
..PluginLoaderV2::new()
}
}
#[test]
fn load_all_plugins_strict_fails_on_missing_library() {
let _guard = EnvGuard::set("HAKO_JOINIR_STRICT", "1");
let loader = loader_with_missing_library("/nonexistent/libnyash_filebox_plugin");
let result = load_all_plugins(&loader);
assert!(result.is_err(), "strict mode must fail when library is missing");
}
#[test]
fn load_all_plugins_best_effort_continues_on_missing_library() {
let _guard = EnvGuard::unset("HAKO_JOINIR_STRICT");
let loader = loader_with_missing_library("/nonexistent/libnyash_filebox_plugin");
let result = load_all_plugins(&loader);
assert!(
result.is_ok(),
"non-strict mode should continue even when a library is missing"
);
}
}

View File

@ -1,4 +1,5 @@
mod config;
mod error_reporter;
mod library;
mod metadata;
mod singletons;