use crate::backend::{Backend, ModelBuilder};
use crate::config::ConfigSpec;
use crate::diagnostics::{TraceEvent, TraceLevel, TraceSink, TraceSinkExt};
use crate::error::{AphelionError, AphelionResult};
use crate::graph::BuildGraph;
use std::collections::HashSet;
use std::time::SystemTime;
#[cfg(feature = "rust-ai-core")]
use crate::rust_ai_core::MemoryTracker;
pub type PreBuildHook = Box<dyn Fn(&BuildContext) -> AphelionResult<()> + Send + Sync>;
pub type PostBuildHook =
Box<dyn Fn(&BuildContext, &BuildGraph) -> AphelionResult<()> + Send + Sync>;
pub type ProgressCallback = Box<dyn Fn(&str, usize, usize) + Send + Sync>;
pub struct BuildContext<'a> {
pub backend: &'a dyn Backend,
pub trace: &'a dyn TraceSink,
#[cfg(feature = "rust-ai-core")]
pub memory_tracker: Option<&'a MemoryTracker>,
}
impl<'a> BuildContext<'a> {
#[cfg(feature = "rust-ai-core")]
pub fn new(backend: &'a dyn Backend, trace: &'a dyn TraceSink) -> Self {
Self {
backend,
trace,
memory_tracker: None,
}
}
#[cfg(not(feature = "rust-ai-core"))]
pub fn new(backend: &'a dyn Backend, trace: &'a dyn TraceSink) -> Self {
Self { backend, trace }
}
#[cfg(feature = "rust-ai-core")]
pub fn with_null_backend(
backend: &'a crate::backend::NullBackend,
trace: &'a dyn TraceSink,
) -> Self {
Self {
backend,
trace,
memory_tracker: None,
}
}
#[cfg(not(feature = "rust-ai-core"))]
pub fn with_null_backend(
backend: &'a crate::backend::NullBackend,
trace: &'a dyn TraceSink,
) -> Self {
Self { backend, trace }
}
#[cfg(feature = "rust-ai-core")]
pub fn with_memory_tracker(
backend: &'a dyn Backend,
trace: &'a dyn TraceSink,
memory_tracker: &'a MemoryTracker,
) -> Self {
Self {
backend,
trace,
memory_tracker: Some(memory_tracker),
}
}
#[cfg(feature = "rust-ai-core")]
pub fn would_fit(&self, bytes: usize) -> bool {
self.memory_tracker
.map(|t| t.would_fit(bytes))
.unwrap_or(true)
}
#[cfg(feature = "rust-ai-core")]
pub fn allocate(&self, bytes: usize) -> AphelionResult<()> {
if let Some(tracker) = self.memory_tracker {
tracker
.allocate(bytes)
.map_err(|e| AphelionError::backend(format!("OOM: {}", e)))?;
}
Ok(())
}
#[cfg(feature = "rust-ai-core")]
pub fn deallocate(&self, bytes: usize) {
if let Some(tracker) = self.memory_tracker {
tracker.deallocate(bytes);
}
}
#[cfg(feature = "rust-ai-core")]
pub fn allocated_bytes(&self) -> Option<usize> {
self.memory_tracker.map(|t| t.allocated_bytes())
}
#[cfg(feature = "rust-ai-core")]
pub fn peak_bytes(&self) -> Option<usize> {
self.memory_tracker.map(|t| t.peak_bytes())
}
}
pub trait PipelineStage: Send + Sync {
fn name(&self) -> &str;
fn execute(&self, ctx: &BuildContext, graph: &mut BuildGraph) -> AphelionResult<()>;
}
#[cfg(feature = "tokio")]
pub trait AsyncPipelineStage: Send + Sync {
fn name(&self) -> &str;
fn execute_async<'a>(
&'a self,
ctx: &'a BuildContext<'_>,
graph: &'a mut BuildGraph,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = AphelionResult<()>> + Send + 'a>>;
}
#[derive(Default)]
pub struct PipelineHooks {
pub pre_build: Vec<PreBuildHook>,
pub post_build: Vec<PostBuildHook>,
}
pub struct BuildPipeline {
stages: Vec<Box<dyn PipelineStage>>,
#[cfg(feature = "tokio")]
async_stages: Vec<Box<dyn AsyncPipelineStage>>,
hooks: PipelineHooks,
skip_stages: HashSet<String>,
on_progress: Option<ProgressCallback>,
}
impl Default for BuildPipeline {
fn default() -> Self {
Self::new()
}
}
impl BuildPipeline {
pub fn new() -> Self {
Self {
stages: Vec::new(),
#[cfg(feature = "tokio")]
async_stages: Vec::new(),
hooks: PipelineHooks::default(),
skip_stages: HashSet::new(),
on_progress: None,
}
}
pub fn standard() -> Self {
Self::new()
.with_stage(Box::new(ValidationStage))
.with_stage(Box::new(HashingStage))
}
pub fn for_training() -> Self {
Self::standard().with_pre_hook(|ctx| {
ctx.trace
.info("pipeline.training", "Starting training pipeline");
Ok(())
})
}
pub fn for_inference() -> Self {
Self::new().with_stage(Box::new(HashingStage))
}
pub fn with_stage(mut self, stage: Box<dyn PipelineStage>) -> Self {
self.stages.push(stage);
self
}
#[cfg(feature = "tokio")]
pub fn with_async_stage(mut self, stage: Box<dyn AsyncPipelineStage>) -> Self {
self.async_stages.push(stage);
self
}
pub fn with_pre_hook<F>(mut self, hook: F) -> Self
where
F: Fn(&BuildContext) -> AphelionResult<()> + Send + Sync + 'static,
{
self.hooks.pre_build.push(Box::new(hook));
self
}
pub fn with_post_hook<F>(mut self, hook: F) -> Self
where
F: Fn(&BuildContext, &BuildGraph) -> AphelionResult<()> + Send + Sync + 'static,
{
self.hooks.post_build.push(Box::new(hook));
self
}
pub fn with_skip_stage(mut self, name: impl Into<String>) -> Self {
self.skip_stages.insert(name.into());
self
}
pub fn with_progress<F>(mut self, callback: F) -> Self
where
F: Fn(&str, usize, usize) + Send + Sync + 'static,
{
self.on_progress = Some(Box::new(callback));
self
}
pub fn execute(
&self,
ctx: &BuildContext<'_>,
mut graph: BuildGraph,
) -> AphelionResult<BuildGraph> {
for hook in &self.hooks.pre_build {
hook(ctx)?;
}
let total_stages = self.stages.len();
for (index, stage) in self.stages.iter().enumerate() {
let stage_name = stage.name();
if self.skip_stages.contains(stage_name) {
if let Some(ref progress) = self.on_progress {
progress(
&format!("{} (skipped)", stage_name),
index + 1,
total_stages,
);
}
continue;
}
if let Some(ref progress) = self.on_progress {
progress(stage_name, index + 1, total_stages);
}
stage.execute(ctx, &mut graph)?;
}
for hook in &self.hooks.post_build {
hook(ctx, &graph)?;
}
Ok(graph)
}
#[cfg(feature = "tokio")]
pub async fn execute_async(
&self,
ctx: &BuildContext<'_>,
mut graph: BuildGraph,
) -> AphelionResult<BuildGraph> {
for hook in &self.hooks.pre_build {
hook(ctx)?;
}
let total_stages = self.async_stages.len();
for (index, stage) in self.async_stages.iter().enumerate() {
let stage_name = stage.name();
if self.skip_stages.contains(stage_name) {
if let Some(ref progress) = self.on_progress {
progress(
&format!("{} (skipped)", stage_name),
index + 1,
total_stages,
);
}
continue;
}
if let Some(ref progress) = self.on_progress {
progress(stage_name, index + 1, total_stages);
}
stage.execute_async(ctx, &mut graph).await?;
}
for hook in &self.hooks.post_build {
hook(ctx, &graph)?;
}
Ok(graph)
}
pub fn build<M: ModelBuilder<Output = BuildGraph>>(
model: &M,
ctx: BuildContext<'_>,
) -> AphelionResult<BuildGraph> {
ctx.trace.record(TraceEvent {
id: "pipeline.start".to_string(),
message: "build started".to_string(),
timestamp: SystemTime::now(),
level: TraceLevel::Info,
span_id: None,
trace_id: None,
});
let graph = model.build(ctx.backend, ctx.trace);
ctx.trace.record(TraceEvent {
id: "pipeline.finish".to_string(),
message: format!("build completed hash={}", graph.stable_hash()),
timestamp: SystemTime::now(),
level: TraceLevel::Info,
span_id: None,
trace_id: None,
});
Ok(graph)
}
pub fn build_with_validation<M>(model: &M, ctx: BuildContext<'_>) -> AphelionResult<BuildGraph>
where
M: ModelBuilder<Output = BuildGraph> + ConfigSpec,
{
let config = model.config();
if config.name.trim().is_empty() {
return Err(AphelionError::config_error("name cannot be empty"));
}
if config.version.trim().is_empty() {
return Err(AphelionError::config_error("version cannot be empty"));
}
ctx.trace.record(TraceEvent {
id: "pipeline.validate".to_string(),
message: format!("validated {}@{}", config.name, config.version),
timestamp: SystemTime::now(),
level: TraceLevel::Info,
span_id: None,
trace_id: None,
});
Self::build(model, ctx)
}
}
pub struct ValidationStage;
impl PipelineStage for ValidationStage {
fn name(&self) -> &str {
"validation"
}
fn execute(&self, ctx: &BuildContext, graph: &mut BuildGraph) -> AphelionResult<()> {
if graph.nodes.is_empty() {
return Err(AphelionError::validation(
"graph must contain at least one node",
));
}
ctx.trace.record(TraceEvent {
id: "stage.validation".to_string(),
message: format!("validated {} nodes", graph.nodes.len()),
timestamp: SystemTime::now(),
level: TraceLevel::Info,
span_id: None,
trace_id: None,
});
Ok(())
}
}
pub struct HashingStage;
impl PipelineStage for HashingStage {
fn name(&self) -> &str {
"hashing"
}
fn execute(&self, ctx: &BuildContext, graph: &mut BuildGraph) -> AphelionResult<()> {
let hash = graph.stable_hash();
ctx.trace.record(TraceEvent {
id: "stage.hashing".to_string(),
message: format!("computed graph hash: {}", hash),
timestamp: SystemTime::now(),
level: TraceLevel::Info,
span_id: None,
trace_id: None,
});
Ok(())
}
}
#[cfg(feature = "tokio")]
impl AsyncPipelineStage for ValidationStage {
fn name(&self) -> &str {
"validation"
}
fn execute_async<'a>(
&'a self,
ctx: &'a BuildContext<'_>,
graph: &'a mut BuildGraph,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = AphelionResult<()>> + Send + 'a>> {
Box::pin(async move {
self.execute(ctx, graph)
})
}
}
#[cfg(feature = "tokio")]
impl AsyncPipelineStage for HashingStage {
fn name(&self) -> &str {
"hashing"
}
fn execute_async<'a>(
&'a self,
ctx: &'a BuildContext<'_>,
graph: &'a mut BuildGraph,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = AphelionResult<()>> + Send + 'a>> {
Box::pin(async move {
self.execute(ctx, graph)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Backend;
use crate::config::ModelConfig;
use crate::diagnostics::TraceSink;
use std::sync::{Arc, Mutex};
struct MockBackend;
impl Backend for MockBackend {
fn name(&self) -> &str {
"mock"
}
fn device(&self) -> &str {
"mock_device"
}
fn capabilities(&self) -> crate::backend::DeviceCapabilities {
crate::backend::DeviceCapabilities::default()
}
fn is_available(&self) -> bool {
true
}
}
struct MockTraceSink {
events: Arc<Mutex<Vec<String>>>,
}
impl MockTraceSink {
fn new() -> Self {
Self {
events: Arc::new(Mutex::new(Vec::new())),
}
}
fn get_events(&self) -> Vec<String> {
self.events.lock().unwrap().clone()
}
}
impl TraceSink for MockTraceSink {
fn record(&self, event: TraceEvent) {
let mut events = self.events.lock().unwrap();
events.push(format!("{}: {}", event.id, event.message));
}
}
struct RecordingStage {
name: String,
executed: Arc<Mutex<Vec<String>>>,
}
impl RecordingStage {
fn new(name: &str, executed: Arc<Mutex<Vec<String>>>) -> Self {
Self {
name: name.to_string(),
executed,
}
}
}
impl PipelineStage for RecordingStage {
fn name(&self) -> &str {
&self.name
}
fn execute(&self, _ctx: &BuildContext, _graph: &mut BuildGraph) -> AphelionResult<()> {
self.executed.lock().unwrap().push(self.name.clone());
Ok(())
}
}
#[test]
fn test_stage_execution_order() {
let executed = Arc::new(Mutex::new(Vec::new()));
let stage1 = Box::new(RecordingStage::new("stage1", Arc::clone(&executed)));
let stage2 = Box::new(RecordingStage::new("stage2", Arc::clone(&executed)));
let stage3 = Box::new(RecordingStage::new("stage3", Arc::clone(&executed)));
let pipeline = BuildPipeline::new()
.with_stage(stage1)
.with_stage(stage2)
.with_stage(stage3);
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
let execution_order = executed.lock().unwrap().clone();
assert_eq!(execution_order, vec!["stage1", "stage2", "stage3"]);
}
#[test]
fn test_pre_hook_invocation() {
let pre_hook_called = Arc::new(Mutex::new(false));
let pre_hook_called_clone = Arc::clone(&pre_hook_called);
let pipeline = BuildPipeline::new().with_pre_hook(move |_ctx| {
*pre_hook_called_clone.lock().unwrap() = true;
Ok(())
});
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
assert!(*pre_hook_called.lock().unwrap());
}
#[test]
fn test_post_hook_invocation() {
let post_hook_called = Arc::new(Mutex::new(false));
let post_hook_called_clone = Arc::clone(&post_hook_called);
let pipeline = BuildPipeline::new().with_post_hook(move |_ctx, _graph| {
*post_hook_called_clone.lock().unwrap() = true;
Ok(())
});
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
assert!(*post_hook_called.lock().unwrap());
}
#[test]
fn test_stage_skipping() {
let executed = Arc::new(Mutex::new(Vec::new()));
let stage1 = Box::new(RecordingStage::new("stage1", Arc::clone(&executed)));
let stage2 = Box::new(RecordingStage::new("stage2", Arc::clone(&executed)));
let stage3 = Box::new(RecordingStage::new("stage3", Arc::clone(&executed)));
let pipeline = BuildPipeline::new()
.with_stage(stage1)
.with_stage(stage2)
.with_stage(stage3)
.with_skip_stage("stage2");
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
let execution_order = executed.lock().unwrap().clone();
assert_eq!(execution_order, vec!["stage1", "stage3"]);
}
#[test]
fn test_progress_callback() {
let progress_calls = Arc::new(Mutex::new(Vec::new()));
let progress_calls_clone = Arc::clone(&progress_calls);
let stage1 = Box::new(RecordingStage::new(
"stage1",
Arc::new(Mutex::new(Vec::new())),
));
let stage2 = Box::new(RecordingStage::new(
"stage2",
Arc::new(Mutex::new(Vec::new())),
));
let pipeline = BuildPipeline::new()
.with_stage(stage1)
.with_stage(stage2)
.with_progress(move |name, current, total| {
progress_calls_clone
.lock()
.unwrap()
.push((name.to_string(), current, total));
});
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
let calls = progress_calls.lock().unwrap().clone();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0], ("stage1".to_string(), 1, 2));
assert_eq!(calls[1], ("stage2".to_string(), 2, 2));
}
#[test]
fn test_progress_callback_with_skipped_stages() {
let progress_calls = Arc::new(Mutex::new(Vec::new()));
let progress_calls_clone = Arc::clone(&progress_calls);
let stage1 = Box::new(RecordingStage::new(
"stage1",
Arc::new(Mutex::new(Vec::new())),
));
let stage2 = Box::new(RecordingStage::new(
"stage2",
Arc::new(Mutex::new(Vec::new())),
));
let stage3 = Box::new(RecordingStage::new(
"stage3",
Arc::new(Mutex::new(Vec::new())),
));
let pipeline = BuildPipeline::new()
.with_stage(stage1)
.with_stage(stage2)
.with_stage(stage3)
.with_skip_stage("stage2")
.with_progress(move |name, current, total| {
progress_calls_clone
.lock()
.unwrap()
.push((name.to_string(), current, total));
});
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
let calls = progress_calls.lock().unwrap().clone();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0], ("stage1".to_string(), 1, 3));
assert!(calls[1].0.contains("(skipped)"));
assert_eq!(calls[2], ("stage3".to_string(), 3, 3));
}
#[test]
fn test_validation_stage() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let result = ValidationStage.execute(&ctx, &mut graph);
assert!(result.is_err());
}
#[test]
fn test_validation_stage_with_nodes() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let config = ModelConfig::new("test", "1.0");
graph.add_node("test_node", config);
let result = ValidationStage.execute(&ctx, &mut graph);
assert!(result.is_ok());
let events = trace_sink.get_events();
assert!(events[0].contains("validated 1 nodes"));
}
#[test]
fn test_hashing_stage() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let config = ModelConfig::new("test", "1.0");
graph.add_node("test_node", config);
let result = HashingStage.execute(&ctx, &mut graph);
assert!(result.is_ok());
let events = trace_sink.get_events();
assert!(events[0].contains("computed graph hash:"));
}
#[test]
fn test_multiple_pre_hooks() {
let hook1_called = Arc::new(Mutex::new(false));
let hook2_called = Arc::new(Mutex::new(false));
let hook1_called_clone = Arc::clone(&hook1_called);
let hook2_called_clone = Arc::clone(&hook2_called);
let pipeline = BuildPipeline::new()
.with_pre_hook(move |_ctx| {
*hook1_called_clone.lock().unwrap() = true;
Ok(())
})
.with_pre_hook(move |_ctx| {
*hook2_called_clone.lock().unwrap() = true;
Ok(())
});
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let _result = pipeline.execute(&ctx, graph);
assert!(*hook1_called.lock().unwrap());
assert!(*hook2_called.lock().unwrap());
}
#[test]
fn test_hook_error_propagation() {
let pipeline =
BuildPipeline::new().with_pre_hook(|_ctx| Err(AphelionError::validation("test error")));
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let graph = BuildGraph::default();
let result = pipeline.execute(&ctx, graph);
assert!(result.is_err());
}
#[test]
fn test_build_context_new() {
let backend = MockBackend;
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&backend, &trace_sink);
assert_eq!(ctx.backend.name(), "mock");
assert_eq!(ctx.backend.device(), "mock_device");
assert!(ctx.backend.is_available());
}
#[test]
fn test_build_context_with_null_backend() {
let backend = crate::backend::NullBackend::cpu();
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::with_null_backend(&backend, &trace_sink);
assert_eq!(ctx.backend.name(), "null");
assert_eq!(ctx.backend.device(), "cpu");
assert!(ctx.backend.is_available());
}
#[test]
fn test_build_context_new_method() {
let backend = MockBackend;
let trace_sink = MockTraceSink::new();
let ctx1 = BuildContext::new(&backend, &trace_sink);
let ctx2 = BuildContext::new(&backend, &trace_sink);
assert_eq!(ctx1.backend.name(), ctx2.backend.name());
assert_eq!(ctx1.backend.device(), ctx2.backend.device());
}
#[test]
fn test_standard_pipeline_has_validation_and_hashing() {
let pipeline = BuildPipeline::standard();
assert_eq!(pipeline.stages.len(), 2);
let stage_names: Vec<&str> = pipeline.stages.iter().map(|s| s.name()).collect();
assert_eq!(stage_names, vec!["validation", "hashing"]);
}
#[test]
fn test_training_pipeline_logs_start() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let config = ModelConfig::new("test", "1.0");
graph.add_node("test_node", config);
let pipeline = BuildPipeline::for_training();
let _result = pipeline.execute(&ctx, graph);
let events = trace_sink.get_events();
assert!(events.len() >= 2);
assert!(events
.iter()
.any(|e| e.contains("pipeline.training") && e.contains("Starting training pipeline")));
}
#[test]
fn test_training_pipeline_has_standard_stages() {
let pipeline = BuildPipeline::for_training();
assert_eq!(pipeline.stages.len(), 2);
let stage_names: Vec<&str> = pipeline.stages.iter().map(|s| s.name()).collect();
assert_eq!(stage_names, vec!["validation", "hashing"]);
assert_eq!(pipeline.hooks.pre_build.len(), 1);
}
#[test]
fn test_inference_pipeline_minimal() {
let pipeline = BuildPipeline::for_inference();
assert_eq!(pipeline.stages.len(), 1);
let stage_names: Vec<&str> = pipeline.stages.iter().map(|s| s.name()).collect();
assert_eq!(stage_names, vec!["hashing"]);
assert_eq!(pipeline.hooks.pre_build.len(), 0);
assert_eq!(pipeline.hooks.post_build.len(), 0);
}
#[test]
fn test_inference_pipeline_execution() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let config = ModelConfig::new("test", "1.0");
graph.add_node("test_node", config);
let pipeline = BuildPipeline::for_inference();
let result = pipeline.execute(&ctx, graph);
assert!(result.is_ok());
let events = trace_sink.get_events();
assert!(events.iter().any(|e| e.contains("computed graph hash")));
assert!(!events.iter().any(|e| e.contains("validated")));
}
#[test]
fn test_standard_pipeline_execution() {
let trace_sink = MockTraceSink::new();
let ctx = BuildContext::new(&MockBackend, &trace_sink);
let mut graph = BuildGraph::default();
let config = ModelConfig::new("test", "1.0");
graph.add_node("test_node", config);
let pipeline = BuildPipeline::standard();
let result = pipeline.execute(&ctx, graph);
assert!(result.is_ok());
let events = trace_sink.get_events();
assert!(events.iter().any(|e| e.contains("validated 1 nodes")));
assert!(events.iter().any(|e| e.contains("computed graph hash")));
}
#[test]
fn test_preset_pipelines_are_extensible() {
let pipeline = BuildPipeline::standard().with_progress(|name, current, total| {
let _ = (name, current, total);
});
let extended = pipeline.with_stage(Box::new(ValidationStage));
assert_eq!(extended.stages.len(), 3);
}
}