flowcode_core/executor/
mod.rs

1// Command execution logic
2use crate::parser::ParsedCommand;
3// use crate::commands; // To access combine, predict etc.
4use crate::logger::Logger; // Import Logger
5use crate::ast::ArgValue;
6// use crate::util; // kept for legacy commands (to be removed later)
7use crate::error::FCError; // Import FCError
8use crate::spec::{self, CommandSpec}; // Import spec items
9// Allow unused import for future use
10#[allow(unused_imports)]
11use crate::types::{ValueKind, TypedValue};
12
13// Define a result type for execution
14#[derive(Debug, PartialEq)]
15pub enum ExecuteResult {
16    Success(String),      // Unified success output message
17    Error(FCError),       // Error object
18    NoOutput,             // For commands that don't produce direct output
19}
20
21pub struct Executor;
22
23impl Executor {
24    pub fn new() -> Self {
25        Executor
26    }
27
28    // Updated to accept a mutable reference to Logger
29    pub fn execute_command(&self, parsed_command: &ParsedCommand, logger: &mut Logger) -> ExecuteResult {
30        logger.log_action(format!("Executing {}", parsed_command.name.to_uppercase()));
31        let mut _log_output = true; // Most commands should have their output logged
32        let _log_exempt; // Specific commands like 'help' or 'show' might be exempt from logging the command itself
33
34        let result = match parsed_command.name.as_str() {
35            "combine" => {
36                _log_output = false;
37                _log_exempt = true; // prevent duplicate generic logging
38
39                // Convert arguments into TypedValue list for consistent logging/debug
40                let mut typed_args: Vec<TypedValue> = Vec::new();
41                for arg in &parsed_command.args {
42                    match arg {
43                        ArgValue::Number(n) => typed_args.push(TypedValue { kind: ValueKind::Number, value: ArgValue::Number(*n) }),
44                        ArgValue::String(s) => typed_args.push(TypedValue { kind: ValueKind::String, value: ArgValue::String(s.clone()) }),
45                        ArgValue::Bool(b) => typed_args.push(TypedValue { kind: ValueKind::Bool, value: ArgValue::Bool(*b) }),
46                        ArgValue::Null => typed_args.push(TypedValue { kind: ValueKind::Null, value: ArgValue::Null }),
47                        ArgValue::Table(tbl) => typed_args.push(TypedValue { kind: ValueKind::Table, value: ArgValue::Table(tbl.clone()) }),
48                    }
49                }
50
51                // Validate all args are numeric
52                if !typed_args.iter().all(|arg| arg.kind == ValueKind::Number) {
53                    logger.log_action(format!("Attempted COMBINE with invalid args: {:?}", typed_args));
54                    return ExecuteResult::Error(FCError::InvalidArgument("combine command requires numeric arguments".to_string()));
55                }
56
57                // Compute the sum. An empty list results in "-0" according to tests.
58                let sum: f64 = typed_args.iter().map(|arg| match arg.value {
59                    ArgValue::Number(n) => n,
60                    _ => 0.0, // unreachable due to validation, but keeps match exhaustive
61                }).sum();
62
63                let sum_string = if typed_args.is_empty() {
64                    "-0".to_string()
65                } else {
66                    // Remove any trailing .0 for integers to match expected string (e.g., 60 instead of 60.0)
67                    let s = sum.to_string();
68                    if s.ends_with(".0") {
69                        s.trim_end_matches(".0").to_string()
70                    } else {
71                        s
72                    }
73                };
74
75                logger.log_action(format!("Executed COMBINE with args {:?} -> {}", typed_args, sum_string));
76                ExecuteResult::Success(sum_string)
77            }
78            "predict" => {
79                let mut typed_args: Vec<TypedValue> = Vec::new();
80                for arg in &parsed_command.args {
81                    match arg {
82                        ArgValue::Number(n) => typed_args.push(TypedValue { kind: ValueKind::Number, value: ArgValue::Number(*n) }),
83                        ArgValue::String(s) => typed_args.push(TypedValue { kind: ValueKind::String, value: ArgValue::String(s.clone()) }),
84                        ArgValue::Bool(b) => typed_args.push(TypedValue { kind: ValueKind::Bool, value: ArgValue::Bool(*b) }),
85                        ArgValue::Null => typed_args.push(TypedValue { kind: ValueKind::Null, value: ArgValue::Null }),
86                        ArgValue::Table(tbl) => typed_args.push(TypedValue { kind: ValueKind::Table, value: ArgValue::Table(tbl.clone()) }),
87                    }
88                }
89                if typed_args.len() < 2 {
90                    logger.log_action(format!("Attempted PREDICT with insufficient args: {:?}", typed_args));
91                    _log_exempt = true; // prevent duplicate generic logging
92                    return ExecuteResult::Error(FCError::InsufficientData("predict command requires at least two numeric arguments".to_string()));
93                }
94                if typed_args.iter().all(|arg| arg.kind == ValueKind::Number) {
95                    let last_two: Vec<f64> = typed_args.iter().rev().take(2).map(|arg| match arg.value {
96                        ArgValue::Number(n) => n,
97                        _ => unreachable!(),
98                    }).collect();
99                    let prediction = 2.0 * last_two[0] - last_two[1];
100                    logger.log_action(format!("Executed PREDICT with args {:?} -> Prediction: {}", typed_args, prediction));
101                    _log_exempt = true; // prevent duplicate generic logging
102                    ExecuteResult::Success(format!("Prediction: {}", prediction))
103                } else {
104                    logger.log_action(format!("Attempted PREDICT with invalid args: {:?}", typed_args));
105                    _log_exempt = true; // prevent duplicate generic logging
106                    ExecuteResult::Error(FCError::InvalidArgument("predict command requires numeric arguments".to_string()))
107                }
108            }
109            "show" => {
110                _log_output = false; // 'show' output itself is not logged as a result
111                _log_exempt = true;  // 'show' command itself is not logged
112                if !parsed_command.args.is_empty() {
113                    return ExecuteResult::Error(FCError::InvalidArgument("show command does not take arguments".to_string()));
114                }
115                let logs = logger.get_logs();
116                if logs.is_empty() {
117                    ExecuteResult::Success("No logs available.".to_string())
118                } else {
119                    ExecuteResult::Success(logs.join("\n"))
120                }
121            }
122            "help" => {
123                _log_output = false; // Help output isn't logged as a result
124                _log_exempt = true;  // 'help' command itself isn't logged
125                self.execute_help(parsed_command)
126            }
127            _ => {
128                // Fallback or for commands defined in SPECS but not yet implemented in execute_command:
129                _log_exempt = true; // do not log unknown/unimplemented commands in logger
130                ExecuteResult::Error(FCError::UnknownCommand(format!("Command '{}' is recognized but not implemented.", parsed_command.name))) // E9xx for internal/unexpected issues
131            }
132        };
133
134        if matches!(result, ExecuteResult::Success(_)) {
135            _log_output = false; // Example condition, adjust as needed
136        }
137
138        if _log_exempt {
139            return result;
140        }
141        // Generic logging for any command execution error not handled above
142        match &result {
143            ExecuteResult::Error(err) => {
144                logger.log_action(format!("ERROR: Command '{}' failed: {}", parsed_command.name, err));
145            }
146            _ => {}
147        }
148        result
149    }
150
151    // Helper method for the 'help' command
152    fn execute_help(&self, command: &ParsedCommand) -> ExecuteResult {
153        let mut target_command_name: Option<&str> = None;
154        let mut wants_json = false;
155
156        for arg in &command.args {
157            match arg {
158                ArgValue::String(s) if s == "--json" || s == "-j" => {
159                    wants_json = true;
160                }
161                ArgValue::String(s) if !s.starts_with('-') && target_command_name.is_none() => {
162                    target_command_name = Some(s);
163                }
164                _ => {
165                    return ExecuteResult::Error(FCError::InvalidArgument(format!("Invalid argument for help: '{}'. Syntax: help [command_name] [--json | -j]", arg)));
166                }
167            }
168        }
169
170        if wants_json {
171            #[cfg(feature = "json")]
172            {
173                let result_json = if let Some(cmd_name) = target_command_name {
174                    if let Some(spec) = spec::SPECS.iter().find(|s| s.name == cmd_name) {
175                        serde_json::to_string_pretty(spec)
176                    } else {
177                        return ExecuteResult::Error(FCError::UnknownCommand(format!("Cannot provide help for unknown command: '{}'", cmd_name)));
178                    }
179                } else {
180                    spec::get_specs_json()
181                };
182
183                match result_json {
184                    Ok(json_string) => ExecuteResult::Success(json_string),
185                    Err(e) => ExecuteResult::Error(FCError::InternalError(format!("Failed to serialize help to JSON: {}", e))),
186                }
187            }
188            #[cfg(not(feature = "json"))]
189            {
190                ExecuteResult::Error(FCError::InternalError("JSON output for help is not available because the 'json' feature was not enabled at compile time.".to_string()))
191            }
192        } else {
193            // Human-readable format
194            if let Some(cmd_name) = target_command_name {
195                if let Some(spec) = spec::SPECS.iter().find(|s| s.name == cmd_name) {
196                    ExecuteResult::Success(self.format_single_command_help(spec))
197                } else {
198                    ExecuteResult::Error(FCError::UnknownCommand(format!("Cannot provide help for unknown command: '{}'", cmd_name)))
199                }
200            } else {
201                ExecuteResult::Success(self.format_all_commands_help())
202            }
203        }
204    }
205
206    fn format_single_command_help(&self, spec: &CommandSpec) -> String {
207        let mut help_text = format!(
208            "Command: {}\nDescription: {}\nSyntax: {}\n",
209            spec.name, spec.description, spec.syntax
210        );
211        if !spec.arguments.is_empty() {
212            help_text.push_str("\nArguments:\n");
213            for arg_spec in spec.arguments {
214                let required_str = if arg_spec.required { "(required)" } else { "(optional)" };
215                help_text.push_str(&format!(
216                    "  {} - {} {}\n",
217                    arg_spec.name, arg_spec.description, required_str
218                ));
219            }
220        }
221        help_text
222    }
223
224    fn format_all_commands_help(&self) -> String {
225        let mut help_text = "Available commands:\n\n".to_string();
226        for spec in spec::SPECS.iter() {
227            help_text.push_str(&format!(
228                "{:<15} - {}\n{:<15}   Syntax: {}\n\n",
229                spec.name,
230                spec.description,
231                "", // for alignment
232                spec.syntax
233            ));
234        }
235        help_text.push_str("Type 'help <command_name>' for more details on a specific command.\n");
236        help_text.push_str("Type 'help --json' or 'help <command_name> --json' for JSON output (if enabled).");
237        help_text
238    }
239}
240
241/// Execution context that holds the state of the current command execution.
242pub struct ExecutionContext {
243    pub current_values: Vec<TypedValue>,
244}
245
246impl ExecutionContext {
247    pub fn new() -> Self {
248        ExecutionContext {
249            current_values: Vec::new(),
250        }
251    }
252
253    pub fn add_value(&mut self, value: TypedValue) {
254        self.current_values.push(value);
255    }
256
257    pub fn get_value(&self, index: usize) -> Option<&TypedValue> {
258        self.current_values.get(index)
259    }
260}