flowcode-core 0.4.3-alpha

Core execution engine for FlowCode data scripting language
Documentation
// Command execution logic
use crate::parser::ParsedCommand;
// use crate::commands; // To access combine, predict etc.
use crate::logger::Logger; // Import Logger
use crate::ast::ArgValue;
// use crate::util; // kept for legacy commands (to be removed later)
use crate::error::FCError; // Import FCError
use crate::spec::{self, CommandSpec}; // Import spec items
// Allow unused import for future use
#[allow(unused_imports)]
use crate::types::{ValueKind, TypedValue};

#[cfg(feature = "duckdb")]
pub mod backend;
#[cfg(feature = "duckdb")]
use backend::Backend;

// Define a result type for execution
#[derive(Debug, PartialEq)]
pub enum ExecuteResult {
    Success(String),      // Unified success output message
    Error(FCError),       // Error object
    NoOutput,             // For commands that don't produce direct output
}

#[allow(dead_code)]
pub struct Executor {
    #[cfg(feature = "duckdb")]
    backend: Option<Box<dyn Backend + Send + Sync + 'static>>,
    #[cfg(feature = "duckdb")]
    state: std::collections::HashMap<String, String>,
}

impl Executor {
    pub fn new() -> Self {
        Executor {
            #[cfg(feature = "duckdb")]
            backend: None,
            #[cfg(feature = "duckdb")]
            state: std::collections::HashMap::new(),
        }
    }

    /// Creates a new Executor with a custom backend.
    #[cfg(feature = "duckdb")]
    pub fn new_with_backend<T>(backend: Box<T>) -> Self 
    where 
        T: Backend + Send + Sync + 'static,
    {
        Executor {
            backend: Some(backend),
            ..Executor::new()
        }
    }

    // Updated to accept a mutable reference to Logger
    pub fn execute_command(&self, parsed_command: &ParsedCommand, logger: &mut Logger) -> ExecuteResult {
        logger.log_action(format!("Executing {}", parsed_command.name.to_uppercase()));
        let mut _log_output = true; // Most commands should have their output logged
        let _log_exempt; // Specific commands like 'help' or 'show' might be exempt from logging the command itself

        // Check if DuckDB backend is available and should be used
        #[cfg(feature = "duckdb")]
        {
            if let Some(ref backend) = self.backend {
                logger.log_action(format!("Using DuckDB backend for {}", parsed_command.name));
                return match backend.execute(parsed_command) {
                    Ok(result) => ExecuteResult::Success(result),
                    Err(e) => ExecuteResult::Error(FCError::BackendError(format!("DuckDB backend error: {}", e))),
                };
            }
        }

        let result = match parsed_command.name.as_str() {
            "combine" => {
                _log_output = false;
                _log_exempt = true; // prevent duplicate generic logging

                // Convert arguments into TypedValue list for consistent logging/debug
                let mut typed_args: Vec<TypedValue> = Vec::new();
                for arg in &parsed_command.args {
                    match arg {
                        ArgValue::Number(n) => typed_args.push(TypedValue { kind: ValueKind::Number, value: ArgValue::Number(*n) }),
                        ArgValue::String(s) => typed_args.push(TypedValue { kind: ValueKind::String, value: ArgValue::String(s.clone()) }),
                        ArgValue::Bool(b) => typed_args.push(TypedValue { kind: ValueKind::Bool, value: ArgValue::Bool(*b) }),
                        ArgValue::Null => typed_args.push(TypedValue { kind: ValueKind::Null, value: ArgValue::Null }),
                        ArgValue::Table(tbl) => typed_args.push(TypedValue { kind: ValueKind::Table, value: ArgValue::Table(tbl.clone()) }),
                    }
                }

                // Validate all args are numeric
                if !typed_args.iter().all(|arg| arg.kind == ValueKind::Number) {
                    logger.log_action(format!("Attempted COMBINE with invalid args: {:?}", typed_args));
                    return ExecuteResult::Error(FCError::InvalidArgument("combine command requires numeric arguments".to_string()));
                }

                // Compute the sum. An empty list results in "-0" according to tests.
                let sum: f64 = typed_args.iter().map(|arg| match arg.value {
                    ArgValue::Number(n) => n,
                    _ => 0.0, // unreachable due to validation, but keeps match exhaustive
                }).sum();

                let sum_string = if typed_args.is_empty() {
                    "-0".to_string()
                } else {
                    // Remove any trailing .0 for integers to match expected string (e.g., 60 instead of 60.0)
                    let s = sum.to_string();
                    if s.ends_with(".0") {
                        s.trim_end_matches(".0").to_string()
                    } else {
                        s
                    }
                };

                logger.log_action(format!("Executed COMBINE with args {:?} -> {}", typed_args, sum_string));
                ExecuteResult::Success(sum_string)
            }
            "predict" => {
                let mut typed_args: Vec<TypedValue> = Vec::new();
                for arg in &parsed_command.args {
                    match arg {
                        ArgValue::Number(n) => typed_args.push(TypedValue { kind: ValueKind::Number, value: ArgValue::Number(*n) }),
                        ArgValue::String(s) => typed_args.push(TypedValue { kind: ValueKind::String, value: ArgValue::String(s.clone()) }),
                        ArgValue::Bool(b) => typed_args.push(TypedValue { kind: ValueKind::Bool, value: ArgValue::Bool(*b) }),
                        ArgValue::Null => typed_args.push(TypedValue { kind: ValueKind::Null, value: ArgValue::Null }),
                        ArgValue::Table(tbl) => typed_args.push(TypedValue { kind: ValueKind::Table, value: ArgValue::Table(tbl.clone()) }),
                    }
                }
                if typed_args.len() < 2 {
                    logger.log_action(format!("Attempted PREDICT with insufficient args: {:?}", typed_args));
                    _log_exempt = true; // prevent duplicate generic logging
                    return ExecuteResult::Error(FCError::InsufficientData("predict command requires at least two numeric arguments".to_string()));
                }
                if typed_args.iter().all(|arg| arg.kind == ValueKind::Number) {
                    let last_two: Vec<f64> = typed_args.iter().rev().take(2).map(|arg| match arg.value {
                        ArgValue::Number(n) => n,
                        _ => unreachable!(),
                    }).collect();
                    let prediction = 2.0 * last_two[0] - last_two[1];
                    logger.log_action(format!("Executed PREDICT with args {:?} -> Prediction: {}", typed_args, prediction));
                    _log_exempt = true; // prevent duplicate generic logging
                    ExecuteResult::Success(format!("Prediction: {}", prediction))
                } else {
                    logger.log_action(format!("Attempted PREDICT with invalid args: {:?}", typed_args));
                    _log_exempt = true; // prevent duplicate generic logging
                    ExecuteResult::Error(FCError::InvalidArgument("predict command requires numeric arguments".to_string()))
                }
            }
            "show" => {
                _log_output = false; // 'show' output itself is not logged as a result
                _log_exempt = true;  // 'show' command itself is not logged
                if !parsed_command.args.is_empty() {
                    return ExecuteResult::Error(FCError::InvalidArgument("show command does not take arguments".to_string()));
                }
                let logs = logger.get_logs();
                if logs.is_empty() {
                    ExecuteResult::Success("No logs available.".to_string())
                } else {
                    ExecuteResult::Success(logs.join("\n"))
                }
            }
            "help" => {
                _log_output = false; // Help output isn't logged as a result
                _log_exempt = true;  // 'help' command itself isn't logged
                self.execute_help(parsed_command)
            }
            _ => {
                // Fallback or for commands defined in SPECS but not yet implemented in execute_command:
                _log_exempt = true; // do not log unknown/unimplemented commands in logger
                ExecuteResult::Error(FCError::UnknownCommand(format!("Command '{}' is recognized but not implemented.", parsed_command.name))) // E9xx for internal/unexpected issues
            }
        };

        if matches!(result, ExecuteResult::Success(_)) {
            _log_output = false; // Example condition, adjust as needed
        }

        if _log_exempt {
            return result;
        }
        // Generic logging for any command execution error not handled above
        match &result {
            ExecuteResult::Error(err) => {
                logger.log_action(format!("ERROR: Command '{}' failed: {}", parsed_command.name, err));
            }
            _ => {}
        }
        result
    }

    // Helper method for the 'help' command
    fn execute_help(&self, command: &ParsedCommand) -> ExecuteResult {
        let mut target_command_name: Option<&str> = None;
        let mut wants_json = false;

        for arg in &command.args {
            match arg {
                ArgValue::String(s) if s == "--json" || s == "-j" => {
                    wants_json = true;
                }
                ArgValue::String(s) if !s.starts_with('-') && target_command_name.is_none() => {
                    target_command_name = Some(s);
                }
                _ => {
                    return ExecuteResult::Error(FCError::InvalidArgument(format!("Invalid argument for help: '{}'. Syntax: help [command_name] [--json | -j]", arg)));
                }
            }
        }

        if wants_json {
            #[cfg(feature = "json")]
            {
                let result_json = if let Some(cmd_name) = target_command_name {
                    if let Some(spec) = spec::SPECS.iter().find(|s| s.name == cmd_name) {
                        serde_json::to_string_pretty(spec)
                    } else {
                        return ExecuteResult::Error(FCError::UnknownCommand(format!("Cannot provide help for unknown command: '{}'", cmd_name)));
                    }
                } else {
                    spec::get_specs_json()
                };

                match result_json {
                    Ok(json_string) => ExecuteResult::Success(json_string),
                    Err(e) => ExecuteResult::Error(FCError::InternalError(format!("Failed to serialize help to JSON: {}", e))),
                }
            }
            #[cfg(not(feature = "json"))]
            {
                ExecuteResult::Error(FCError::InternalError("JSON output for help is not available because the 'json' feature was not enabled at compile time.".to_string()))
            }
        } else {
            // Human-readable format
            if let Some(cmd_name) = target_command_name {
                if let Some(spec) = spec::SPECS.iter().find(|s| s.name == cmd_name) {
                    ExecuteResult::Success(self.format_single_command_help(spec))
                } else {
                    ExecuteResult::Error(FCError::UnknownCommand(format!("Cannot provide help for unknown command: '{}'", cmd_name)))
                }
            } else {
                ExecuteResult::Success(self.format_all_commands_help())
            }
        }
    }

    fn format_single_command_help(&self, spec: &CommandSpec) -> String {
        let mut help_text = format!(
            "Command: {}\nDescription: {}\nSyntax: {}\n",
            spec.name, spec.description, spec.syntax
        );
        if !spec.arguments.is_empty() {
            help_text.push_str("\nArguments:\n");
            for arg_spec in spec.arguments {
                let required_str = if arg_spec.required { "(required)" } else { "(optional)" };
                help_text.push_str(&format!(
                    "  {} - {} {}\n",
                    arg_spec.name, arg_spec.description, required_str
                ));
            }
        }
        help_text
    }

    fn format_all_commands_help(&self) -> String {
        let mut help_text = "Available commands:\n\n".to_string();
        for spec in spec::SPECS.iter() {
            help_text.push_str(&format!(
                "{:<15} - {}\n{:<15}   Syntax: {}\n\n",
                spec.name,
                spec.description,
                "", // for alignment
                spec.syntax
            ));
        }
        help_text.push_str("Type 'help <command_name>' for more details on a specific command.\n");
        help_text.push_str("Type 'help --json' or 'help <command_name> --json' for JSON output (if enabled).");
        help_text
    }
}

/// Execution context that holds the state of the current command execution.
pub struct ExecutionContext {
    pub current_values: Vec<TypedValue>,
}

impl ExecutionContext {
    pub fn new() -> Self {
        ExecutionContext {
            current_values: Vec::new(),
        }
    }

    pub fn add_value(&mut self, value: TypedValue) {
        self.current_values.push(value);
    }

    pub fn get_value(&self, index: usize) -> Option<&TypedValue> {
        self.current_values.get(index)
    }
}