fix: Add # comment support and remove hardcoded column lists
- Support # as comment marker like ' in BASIC preprocessor - Remove hardcoded column lists from get_table_field_names() - Let runtime use database schema dynamically via get_table_columns() - Fix SELECT/CASE conversion to add semicolons to body statements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
848b875698
commit
9b86b204f2
1 changed files with 608 additions and 49 deletions
643
src/basic/mod.rs
643
src/basic/mod.rs
|
|
@ -186,6 +186,9 @@ impl ScriptService {
|
||||||
register_string_functions(state.clone(), user.clone(), &mut engine);
|
register_string_functions(state.clone(), user.clone(), &mut engine);
|
||||||
switch_keyword(&state, user.clone(), &mut engine);
|
switch_keyword(&state, user.clone(), &mut engine);
|
||||||
register_http_operations(state.clone(), user.clone(), &mut engine);
|
register_http_operations(state.clone(), user.clone(), &mut engine);
|
||||||
|
// Register SAVE FROM UNSTRUCTURED before regular SAVE to avoid pattern conflicts
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
save_from_unstructured_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
register_data_operations(state.clone(), user.clone(), &mut engine);
|
register_data_operations(state.clone(), user.clone(), &mut engine);
|
||||||
#[cfg(feature = "automation")]
|
#[cfg(feature = "automation")]
|
||||||
webhook_keyword(&state, user.clone(), &mut engine);
|
webhook_keyword(&state, user.clone(), &mut engine);
|
||||||
|
|
@ -223,7 +226,6 @@ impl ScriptService {
|
||||||
register_model_routing_keywords(state.clone(), user.clone(), &mut engine);
|
register_model_routing_keywords(state.clone(), user.clone(), &mut engine);
|
||||||
register_multimodal_keywords(state.clone(), user.clone(), &mut engine);
|
register_multimodal_keywords(state.clone(), user.clone(), &mut engine);
|
||||||
remember_keyword(state.clone(), user.clone(), &mut engine);
|
remember_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
save_from_unstructured_keyword(state.clone(), user.clone(), &mut engine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register USE WEBSITE after all other USE keywords to avoid conflicts
|
// Register USE WEBSITE after all other USE keywords to avoid conflicts
|
||||||
|
|
@ -579,6 +581,7 @@ impl ScriptService {
|
||||||
trimmed.starts_with("DESCRIPTION ") ||
|
trimmed.starts_with("DESCRIPTION ") ||
|
||||||
trimmed.starts_with("DESCRIPTION\t") ||
|
trimmed.starts_with("DESCRIPTION\t") ||
|
||||||
trimmed.starts_with('\'') || // BASIC comment lines
|
trimmed.starts_with('\'') || // BASIC comment lines
|
||||||
|
trimmed.starts_with('#') || // Hash comment lines
|
||||||
trimmed.is_empty())
|
trimmed.is_empty())
|
||||||
})
|
})
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
|
|
@ -589,10 +592,14 @@ impl ScriptService {
|
||||||
// Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings)
|
// Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings)
|
||||||
let script = preprocess_switch(&executable_script);
|
let script = preprocess_switch(&executable_script);
|
||||||
let script = Self::convert_multiword_keywords(&script);
|
let script = Self::convert_multiword_keywords(&script);
|
||||||
|
// Convert FORMAT(expr, pattern) to FORMAT expr pattern for Rhai space-separated function syntax
|
||||||
|
// FORMAT syntax conversion disabled - Rhai supports comma-separated args natively
|
||||||
|
// let script = Self::convert_format_syntax(&script);
|
||||||
// Skip normalize_variables_to_lowercase for tools - it breaks multi-line strings
|
// Skip normalize_variables_to_lowercase for tools - it breaks multi-line strings
|
||||||
// Note: FORMAT is registered as a regular function, so FORMAT(expr, pattern) works directly
|
|
||||||
|
|
||||||
info!("[TOOL] Preprocessed tool script for Rhai compilation");
|
info!("[TOOL] Preprocessed tool script for Rhai compilation");
|
||||||
|
// Convert BEGIN TALK and BEGIN MAIL blocks to single calls
|
||||||
|
let script = crate::basic::compiler::blocks::convert_begin_blocks(&script);
|
||||||
// Convert IF ... THEN / END IF to if ... { }
|
// Convert IF ... THEN / END IF to if ... { }
|
||||||
let script = Self::convert_if_then_syntax(&script);
|
let script = Self::convert_if_then_syntax(&script);
|
||||||
// Convert SELECT ... CASE / END SELECT to match expressions
|
// Convert SELECT ... CASE / END SELECT to match expressions
|
||||||
|
|
@ -619,32 +626,381 @@ impl ScriptService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
/// Convert FORMAT(expr, pattern) to FORMAT expr pattern (custom syntax format)
|
/// Convert FORMAT(expr, pattern) to FORMAT expr pattern (custom syntax format)
|
||||||
/// Also handles RANDOM and other functions that need space-separated arguments
|
/// Also handles RANDOM and other functions that need space-separated arguments
|
||||||
|
/// This properly handles nested function calls by counting parentheses
|
||||||
fn convert_format_syntax(script: &str) -> String {
|
fn convert_format_syntax(script: &str) -> String {
|
||||||
use regex::Regex;
|
let mut result = String::new();
|
||||||
let mut result = script.to_string();
|
let mut chars = script.chars().peekable();
|
||||||
|
let mut i = 0;
|
||||||
|
let bytes = script.as_bytes();
|
||||||
|
|
||||||
// First, process RANDOM to ensure commas are preserved
|
while i < bytes.len() {
|
||||||
// RANDOM(min, max) stays as RANDOM(min, max) - no conversion needed
|
// Check if this is the start of FORMAT(
|
||||||
|
if i + 6 <= bytes.len()
|
||||||
|
&& bytes[i..i+6].eq_ignore_ascii_case(b"FORMAT")
|
||||||
|
&& i + 7 < bytes.len()
|
||||||
|
&& bytes[i + 6] == b'('
|
||||||
|
{
|
||||||
|
// Found FORMAT( - now parse the arguments
|
||||||
|
let mut paren_depth = 1;
|
||||||
|
let mut j = i + 7; // Start after FORMAT(
|
||||||
|
let mut comma_pos = None;
|
||||||
|
|
||||||
// Convert FORMAT(expr, pattern) → FORMAT expr pattern
|
// Find the arguments by tracking parentheses
|
||||||
// Need to handle nested functions carefully
|
while j < bytes.len() && paren_depth > 0 {
|
||||||
// Match FORMAT( ... ) but don't include inner function parentheses
|
match bytes[j] {
|
||||||
// This regex matches FORMAT followed by parentheses containing two comma-separated expressions
|
b'(' => paren_depth += 1,
|
||||||
if let Ok(re) = Regex::new(r"(?i)FORMAT\s*\(([^()]+(?:\([^()]*\)[^()]*)*),([^)]+)\)") {
|
b')' => {
|
||||||
result = re.replace_all(&result, "FORMAT $1$2").to_string();
|
paren_depth -= 1;
|
||||||
|
if paren_depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b',' => {
|
||||||
|
if paren_depth == 1 {
|
||||||
|
// This is the comma separating FORMAT's arguments
|
||||||
|
comma_pos = Some(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(comma) = comma_pos {
|
||||||
|
// Extract the two arguments
|
||||||
|
let expr = &script[i + 7..comma].trim();
|
||||||
|
let pattern = &script[comma + 1..j].trim();
|
||||||
|
|
||||||
|
// Convert to Rhai space-separated syntax
|
||||||
|
// Remove quotes from pattern if present, then add them back in the right format
|
||||||
|
let pattern_clean = pattern.trim_matches('"').trim_matches('\'');
|
||||||
|
result.push_str(&format!("FORMAT ({expr}) (\"{pattern_clean}\")"));
|
||||||
|
|
||||||
|
i = j + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the character as-is
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a single TALK line with ${variable} substitution to proper TALK syntax
|
||||||
|
/// Handles: "Hello ${name}" → TALK "Hello " + name
|
||||||
|
/// Also handles: "Plain text" → TALK "Plain text"
|
||||||
|
/// Also handles function calls: "Value: ${FORMAT(x, "n")}" → TALK "Value: " + FORMAT(x, "n")
|
||||||
|
fn convert_talk_line_with_substitution(line: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = line.chars().peekable();
|
||||||
|
let mut in_substitution = false;
|
||||||
|
let mut current_expr = String::new();
|
||||||
|
let mut current_literal = String::new();
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'$' => {
|
||||||
|
if let Some(&'{') = chars.peek() {
|
||||||
|
// Start of ${...} substitution
|
||||||
|
chars.next(); // consume '{'
|
||||||
|
|
||||||
|
// Add accumulated literal as a string if non-empty
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str("TALK \"");
|
||||||
|
} else {
|
||||||
|
result.push_str(" + \"");
|
||||||
|
}
|
||||||
|
// Escape any quotes in the literal
|
||||||
|
let escaped = current_literal.replace('"', "\\\"");
|
||||||
|
result.push_str(&escaped);
|
||||||
|
result.push('"');
|
||||||
|
current_literal.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
in_substitution = true;
|
||||||
|
current_expr.clear();
|
||||||
|
} else {
|
||||||
|
// Regular $ character, add to literal
|
||||||
|
current_literal.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'}' if in_substitution => {
|
||||||
|
// End of ${...} substitution
|
||||||
|
in_substitution = false;
|
||||||
|
|
||||||
|
// Add the expression (variable or function call)
|
||||||
|
if !current_expr.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str(¤t_expr);
|
||||||
|
} else {
|
||||||
|
result.push_str(" + ");
|
||||||
|
result.push_str(¤t_expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_expr.clear();
|
||||||
|
}
|
||||||
|
_ if in_substitution => {
|
||||||
|
// Collect expression content, tracking parentheses and quotes
|
||||||
|
// This handles function calls like FORMAT(x, "pattern")
|
||||||
|
current_expr.push(c);
|
||||||
|
|
||||||
|
// Track nested parentheses and quoted strings
|
||||||
|
let mut paren_depth: i32 = 0;
|
||||||
|
let mut in_string = false;
|
||||||
|
let mut escape_next = false;
|
||||||
|
|
||||||
|
for ch in current_expr.chars() {
|
||||||
|
if escape_next {
|
||||||
|
escape_next = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
escape_next = true;
|
||||||
|
}
|
||||||
|
'"' if !in_string => {
|
||||||
|
in_string = true;
|
||||||
|
}
|
||||||
|
'"' if in_string => {
|
||||||
|
in_string = false;
|
||||||
|
}
|
||||||
|
'(' if !in_string => {
|
||||||
|
paren_depth += 1;
|
||||||
|
}
|
||||||
|
')' if !in_string => {
|
||||||
|
paren_depth = paren_depth.saturating_sub(1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue collecting expression until we're back at depth 0
|
||||||
|
// The closing '}' will handle the end of substitution
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Regular character, add to literal
|
||||||
|
current_literal.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining literal
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str("TALK \"");
|
||||||
|
} else {
|
||||||
|
result.push_str(" + \"");
|
||||||
|
}
|
||||||
|
let escaped = current_literal.replace('"', "\\\"");
|
||||||
|
result.push_str(&escaped);
|
||||||
|
result.push('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If result is empty (shouldn't happen), just return a TALK with empty string
|
||||||
|
if result.is_empty() {
|
||||||
|
result = "TALK \"\"".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("[TOOL] Converted TALK line: '{}' → '{}'", line, result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a BEGIN MAIL ... END MAIL block to SEND EMAIL call
|
||||||
|
/// Handles multi-line emails with ${variable} substitution
|
||||||
|
/// Uses intermediate variables to reduce expression complexity
|
||||||
|
/// Format:
|
||||||
|
/// BEGIN MAIL recipient
|
||||||
|
/// Subject: Email subject here
|
||||||
|
///
|
||||||
|
/// Body line 1 with ${variable}
|
||||||
|
/// Body line 2 with ${anotherVariable}
|
||||||
|
/// END MAIL
|
||||||
|
fn convert_mail_block(recipient: &str, lines: &[String]) -> String {
|
||||||
|
let mut subject = String::new();
|
||||||
|
let mut body_lines: Vec<String> = Vec::new();
|
||||||
|
let mut in_subject = true;
|
||||||
|
let mut skip_blank = true;
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
// Check if this line is a subject line
|
||||||
|
if line.to_uppercase().starts_with("SUBJECT:") {
|
||||||
|
subject = line[8..].trim().to_string();
|
||||||
|
in_subject = false;
|
||||||
|
skip_blank = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip blank lines after subject
|
||||||
|
if skip_blank && line.trim().is_empty() {
|
||||||
|
skip_blank = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
skip_blank = false;
|
||||||
|
|
||||||
|
// Process body line with ${} substitution
|
||||||
|
let converted = Self::convert_mail_line_with_substitution(line);
|
||||||
|
body_lines.push(converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate code that builds the email body using intermediate variables
|
||||||
|
// This reduces expression complexity for Rhai parser
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
// Create intermediate variables for body chunks (max 5 lines per variable to keep complexity low)
|
||||||
|
let chunk_size = 5;
|
||||||
|
let mut var_count = 0;
|
||||||
|
let mut all_vars: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for chunk in body_lines.chunks(chunk_size) {
|
||||||
|
let var_name = format!("__mail_body_{}__", var_count);
|
||||||
|
all_vars.push(var_name.clone());
|
||||||
|
|
||||||
|
if chunk.len() == 1 {
|
||||||
|
result.push_str(&format!("let {} = {};\n", var_name, chunk[0]));
|
||||||
|
} else {
|
||||||
|
let mut chunk_expr = chunk[0].clone();
|
||||||
|
for line in &chunk[1..] {
|
||||||
|
chunk_expr.push_str(" + \"\\n\" + ");
|
||||||
|
chunk_expr.push_str(line);
|
||||||
|
}
|
||||||
|
result.push_str(&format!("let {} = {};\n", var_name, chunk_expr));
|
||||||
|
}
|
||||||
|
var_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all chunks into final body
|
||||||
|
let body_expr = if all_vars.is_empty() {
|
||||||
|
"\"\"".to_string()
|
||||||
|
} else if all_vars.len() == 1 {
|
||||||
|
all_vars[0].clone()
|
||||||
|
} else {
|
||||||
|
let mut expr = all_vars[0].clone();
|
||||||
|
for var in &all_vars[1..] {
|
||||||
|
expr.push_str(" + \"\\n\" + ");
|
||||||
|
expr.push_str(var);
|
||||||
|
}
|
||||||
|
expr
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the send_mail function call
|
||||||
|
// If recipient contains '@', it's a string literal and needs to be quoted
|
||||||
|
// Otherwise, it's a variable name and should be used as-is
|
||||||
|
let recipient_expr = if recipient.contains('@') {
|
||||||
|
format!("\"{}\"", recipient)
|
||||||
|
} else {
|
||||||
|
recipient.to_string()
|
||||||
|
};
|
||||||
|
result.push_str(&format!("send_mail({}, \"{}\", {}, []);\n", recipient_expr, subject, body_expr));
|
||||||
|
|
||||||
|
log::info!("[TOOL] Converted MAIL block → {}", result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a single mail line with ${variable} substitution to string concatenation
|
||||||
|
/// Similar to TALK substitution but doesn't add "TALK" prefix
|
||||||
|
fn convert_mail_line_with_substitution(line: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = line.chars().peekable();
|
||||||
|
let mut in_substitution = false;
|
||||||
|
let mut current_var = String::new();
|
||||||
|
let mut current_literal = String::new();
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'$' => {
|
||||||
|
if let Some(&'{') = chars.peek() {
|
||||||
|
// Start of ${...} substitution
|
||||||
|
chars.next(); // consume '{'
|
||||||
|
|
||||||
|
// Add accumulated literal as a string if non-empty
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str("\"");
|
||||||
|
result.push_str(¤t_literal.replace('"', "\\\""));
|
||||||
|
result.push('"');
|
||||||
|
} else {
|
||||||
|
result.push_str(" + \"");
|
||||||
|
result.push_str(¤t_literal.replace('"', "\\\""));
|
||||||
|
result.push('"');
|
||||||
|
}
|
||||||
|
current_literal.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
in_substitution = true;
|
||||||
|
current_var.clear();
|
||||||
|
} else {
|
||||||
|
// Regular $ character, add to literal
|
||||||
|
current_literal.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'}' if in_substitution => {
|
||||||
|
// End of ${...} substitution
|
||||||
|
in_substitution = false;
|
||||||
|
|
||||||
|
// Add the variable name
|
||||||
|
if !current_var.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str(¤t_var);
|
||||||
|
} else {
|
||||||
|
result.push_str(" + ");
|
||||||
|
result.push_str(¤t_var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_var.clear();
|
||||||
|
}
|
||||||
|
_ if in_substitution => {
|
||||||
|
// Collect variable name (allow alphanumeric, underscore, and function call syntax)
|
||||||
|
if c.is_alphanumeric() || c == '_' || c == '(' || c == ')' || c == ',' || c == ' ' || c == '\"' {
|
||||||
|
current_var.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Regular character, add to literal
|
||||||
|
if !in_substitution {
|
||||||
|
current_literal.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining literal
|
||||||
|
if !current_literal.is_empty() {
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push_str("\"");
|
||||||
|
result.push_str(¤t_literal.replace('"', "\\\""));
|
||||||
|
result.push('"');
|
||||||
|
} else {
|
||||||
|
result.push_str(" + \"");
|
||||||
|
result.push_str(¤t_literal.replace('"', "\\\""));
|
||||||
|
result.push('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("[TOOL] Converted mail line: '{}' → '{}'", line, result);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert BASIC IF ... THEN / END IF syntax to Rhai's if ... { } syntax
|
/// Convert BASIC IF ... THEN / END IF syntax to Rhai's if ... { } syntax
|
||||||
fn convert_if_then_syntax(script: &str) -> String {
|
pub fn convert_if_then_syntax(script: &str) -> String {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
let mut if_stack: Vec<bool> = Vec::new();
|
let mut if_stack: Vec<bool> = Vec::new();
|
||||||
let mut in_with_block = false;
|
let mut in_with_block = false;
|
||||||
|
let mut in_talk_block = false;
|
||||||
|
let mut talk_block_lines: Vec<String> = Vec::new();
|
||||||
|
let mut in_mail_block = false;
|
||||||
|
let mut mail_recipient = String::new();
|
||||||
|
let mut mail_block_lines: Vec<String> = Vec::new();
|
||||||
|
let mut in_line_continuation = false;
|
||||||
|
|
||||||
log::info!("[TOOL] Converting IF/THEN syntax, input has {} lines", script.lines().count());
|
log::info!("[TOOL] Converting IF/THEN syntax, input has {} lines", script.lines().count());
|
||||||
|
|
||||||
|
|
@ -653,7 +1009,7 @@ impl ScriptService {
|
||||||
let upper = trimmed.to_uppercase();
|
let upper = trimmed.to_uppercase();
|
||||||
|
|
||||||
// Skip empty lines and comments
|
// Skip empty lines and comments
|
||||||
if trimmed.is_empty() || trimmed.starts_with('\'') || trimmed.starts_with("//") {
|
if trimmed.is_empty() || trimmed.starts_with('\'') || trimmed.starts_with('#') || trimmed.starts_with("//") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,6 +1022,19 @@ impl ScriptService {
|
||||||
let condition = &trimmed[3..then_pos].trim();
|
let condition = &trimmed[3..then_pos].trim();
|
||||||
// Convert BASIC "NOT IN" to Rhai "!in"
|
// Convert BASIC "NOT IN" to Rhai "!in"
|
||||||
let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in ");
|
let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in ");
|
||||||
|
// Convert BASIC "AND" to Rhai "&&" and "OR" to Rhai "||"
|
||||||
|
let condition = condition.replace(" AND ", " && ").replace(" and ", " && ")
|
||||||
|
.replace(" OR ", " || ").replace(" or ", " || ");
|
||||||
|
// Convert BASIC "=" to Rhai "==" for comparisons in IF conditions
|
||||||
|
// Skip if it's already a comparison operator (==, !=, <=, >=) or assignment (+=, -=, etc.)
|
||||||
|
let condition = if !condition.contains("==") && !condition.contains("!=")
|
||||||
|
&& !condition.contains("<=") && !condition.contains(">=")
|
||||||
|
&& !condition.contains("+=") && !condition.contains("-=")
|
||||||
|
&& !condition.contains("*=") && !condition.contains("/=") {
|
||||||
|
condition.replace("=", "==")
|
||||||
|
} else {
|
||||||
|
condition.to_string()
|
||||||
|
};
|
||||||
log::info!("[TOOL] Converting IF statement: condition='{}'", condition);
|
log::info!("[TOOL] Converting IF statement: condition='{}'", condition);
|
||||||
result.push_str("if ");
|
result.push_str("if ");
|
||||||
result.push_str(&condition);
|
result.push_str(&condition);
|
||||||
|
|
@ -681,6 +1050,31 @@ impl ScriptService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle ELSEIF ... THEN
|
||||||
|
if upper.starts_with("ELSEIF ") && upper.contains(" THEN") {
|
||||||
|
let then_pos = match upper.find(" THEN") {
|
||||||
|
Some(pos) => pos,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let condition = &trimmed[6..then_pos].trim();
|
||||||
|
let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in ");
|
||||||
|
let condition = condition.replace(" AND ", " && ").replace(" and ", " && ")
|
||||||
|
.replace(" OR ", " || ").replace(" or ", " || ");
|
||||||
|
let condition = if !condition.contains("==") && !condition.contains("!=")
|
||||||
|
&& !condition.contains("<=") && !condition.contains(">=")
|
||||||
|
&& !condition.contains("+=") && !condition.contains("-=")
|
||||||
|
&& !condition.contains("*=") && !condition.contains("/=") {
|
||||||
|
condition.replace("=", "==")
|
||||||
|
} else {
|
||||||
|
condition.to_string()
|
||||||
|
};
|
||||||
|
log::info!("[TOOL] Converting ELSEIF statement: condition='{}'", condition);
|
||||||
|
result.push_str("} else if ");
|
||||||
|
result.push_str(&condition);
|
||||||
|
result.push_str(" {\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle END IF
|
// Handle END IF
|
||||||
if upper == "END IF" {
|
if upper == "END IF" {
|
||||||
log::info!("[TOOL] Converting END IF statement");
|
log::info!("[TOOL] Converting END IF statement");
|
||||||
|
|
@ -709,6 +1103,85 @@ impl ScriptService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle BEGIN TALK ... END TALK (multi-line TALK with ${} substitution)
|
||||||
|
if upper == "BEGIN TALK" {
|
||||||
|
log::info!("[TOOL] Converting BEGIN TALK statement");
|
||||||
|
in_talk_block = true;
|
||||||
|
talk_block_lines.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if upper == "END TALK" {
|
||||||
|
log::info!("[TOOL] Converting END TALK statement, processing {} lines", talk_block_lines.len());
|
||||||
|
in_talk_block = false;
|
||||||
|
|
||||||
|
// Split into multiple TALK statements to avoid expression complexity limit
|
||||||
|
// Use chunks of 5 lines per TALK statement
|
||||||
|
let chunk_size = 5;
|
||||||
|
for (chunk_idx, chunk) in talk_block_lines.chunks(chunk_size).enumerate() {
|
||||||
|
// Convert all talk lines in this chunk to a single TALK statement
|
||||||
|
let mut combined_talk = String::new();
|
||||||
|
for (i, talk_line) in chunk.iter().enumerate() {
|
||||||
|
let converted = Self::convert_talk_line_with_substitution(talk_line);
|
||||||
|
// Remove "TALK " prefix from converted line if present
|
||||||
|
let line_content = if converted.starts_with("TALK ") {
|
||||||
|
converted[5..].trim().to_string()
|
||||||
|
} else {
|
||||||
|
converted
|
||||||
|
};
|
||||||
|
if i > 0 {
|
||||||
|
combined_talk.push_str(" + \"\\n\" + ");
|
||||||
|
}
|
||||||
|
combined_talk.push_str(&line_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate TALK statement for this chunk
|
||||||
|
result.push_str("TALK ");
|
||||||
|
result.push_str(&combined_talk);
|
||||||
|
result.push_str(";\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
talk_block_lines.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a TALK block, collect lines
|
||||||
|
if in_talk_block {
|
||||||
|
// Skip empty lines but preserve them as blank TALK statements if needed
|
||||||
|
talk_block_lines.push(trimmed.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle BEGIN MAIL ... END MAIL (multi-line email with ${} substitution)
|
||||||
|
if upper.starts_with("BEGIN MAIL ") {
|
||||||
|
let recipient = &trimmed[11..].trim(); // Skip "BEGIN MAIL "
|
||||||
|
log::info!("[TOOL] Converting BEGIN MAIL statement: recipient='{}'", recipient);
|
||||||
|
mail_recipient = recipient.to_string();
|
||||||
|
in_mail_block = true;
|
||||||
|
mail_block_lines.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if upper == "END MAIL" {
|
||||||
|
log::info!("[TOOL] Converting END MAIL statement, processing {} lines", mail_block_lines.len());
|
||||||
|
in_mail_block = false;
|
||||||
|
|
||||||
|
// Process the mail block and convert to SEND EMAIL
|
||||||
|
let converted = Self::convert_mail_block(&mail_recipient, &mail_block_lines);
|
||||||
|
result.push_str(&converted);
|
||||||
|
result.push('\n');
|
||||||
|
|
||||||
|
mail_recipient.clear();
|
||||||
|
mail_block_lines.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a MAIL block, collect lines
|
||||||
|
if in_mail_block {
|
||||||
|
mail_block_lines.push(trimmed.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Inside a WITH block - convert property assignments (key = value → key: value)
|
// Inside a WITH block - convert property assignments (key = value → key: value)
|
||||||
if in_with_block {
|
if in_with_block {
|
||||||
// Check if this is a property assignment (identifier = value)
|
// Check if this is a property assignment (identifier = value)
|
||||||
|
|
@ -728,21 +1201,32 @@ impl ScriptService {
|
||||||
result.push_str(" ");
|
result.push_str(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SAVE table, object → INSERT table, object
|
// Handle SAVE table, field1, field2, ... → INSERT "table", #{field1: value1, field2: value2, ...}
|
||||||
// BASIC SAVE uses 2 parameters but Rhai SAVE needs 3
|
|
||||||
// INSERT uses 2 parameters which matches the BASIC syntax
|
|
||||||
if upper.starts_with("SAVE") && upper.contains(',') {
|
if upper.starts_with("SAVE") && upper.contains(',') {
|
||||||
log::info!("[TOOL] Processing SAVE line: '{}'", trimmed);
|
log::info!("[TOOL] Processing SAVE line: '{}'", trimmed);
|
||||||
// Extract table and object name
|
// Extract the part after "SAVE"
|
||||||
let after_save = &trimmed[4..].trim(); // Skip "SAVE"
|
let after_save = &trimmed[4..].trim(); // Skip "SAVE"
|
||||||
let parts: Vec<&str> = after_save.split(',').collect();
|
let parts: Vec<&str> = after_save.split(',').collect();
|
||||||
log::info!("[TOOL] SAVE parts: {:?}", parts);
|
log::info!("[TOOL] SAVE parts: {:?}", parts);
|
||||||
if parts.len() == 2 {
|
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
// First part is the table name (in quotes)
|
||||||
let table = parts[0].trim().trim_matches('"');
|
let table = parts[0].trim().trim_matches('"');
|
||||||
|
|
||||||
|
// For old WITH block syntax (parts.len() == 2), convert to INSERT with object name
|
||||||
|
if parts.len() == 2 {
|
||||||
let object_name = parts[1].trim().trim_end_matches(';');
|
let object_name = parts[1].trim().trim_end_matches(';');
|
||||||
// Convert to INSERT table, object
|
|
||||||
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
|
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
|
||||||
log::info!("[TOOL] Converted SAVE to INSERT: '{}'", converted);
|
log::info!("[TOOL] Converted SAVE to INSERT (old syntax): '{}'", converted);
|
||||||
|
result.push_str(&converted);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For modern direct field list syntax (parts.len() > 2), just pass values as-is
|
||||||
|
// The runtime SAVE handler will match them to database columns by position
|
||||||
|
let values = parts[1..].join(", ");
|
||||||
|
let converted = format!("SAVE \"{}\", {};\n", table, values);
|
||||||
|
log::info!("[TOOL] Keeping SAVE syntax (modern): '{}'", converted);
|
||||||
result.push_str(&converted);
|
result.push_str(&converted);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -787,16 +1271,42 @@ impl ScriptService {
|
||||||
&& !trimmed.contains("*=")
|
&& !trimmed.contains("*=")
|
||||||
&& !trimmed.contains("/=");
|
&& !trimmed.contains("/=");
|
||||||
|
|
||||||
|
// Check for line continuation (BASIC uses comma at end of line)
|
||||||
|
let ends_with_comma = trimmed.ends_with(',');
|
||||||
|
|
||||||
|
// If we're in a line continuation and this is not a variable assignment or statement,
|
||||||
|
// it's likely a string literal continuation - quote it
|
||||||
|
let line_to_process = if in_line_continuation && !is_var_assignment
|
||||||
|
&& !trimmed.contains('=') && !trimmed.starts_with('"') && !upper.starts_with("IF ") {
|
||||||
|
// This is a string literal continuation - quote it and escape any inner quotes
|
||||||
|
let escaped = trimmed.replace('"', "\\\"");
|
||||||
|
format!("\"{}\\n\"", escaped)
|
||||||
|
} else {
|
||||||
|
trimmed.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
if is_var_assignment {
|
if is_var_assignment {
|
||||||
// Add 'let' for variable declarations
|
// Add 'let' for variable declarations, but only if line doesn't already start with let/LET
|
||||||
|
let trimmed_lower = trimmed.to_lowercase();
|
||||||
|
if !trimmed_lower.starts_with("let ") {
|
||||||
result.push_str("let ");
|
result.push_str("let ");
|
||||||
}
|
}
|
||||||
result.push_str(trimmed);
|
}
|
||||||
|
result.push_str(&line_to_process);
|
||||||
// Add semicolon if line doesn't have one and doesn't end with { or }
|
// Add semicolon if line doesn't have one and doesn't end with { or }
|
||||||
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') {
|
// Skip adding semicolons to:
|
||||||
|
// - SELECT/CASE/END SELECT statements (they're converted to if-else later)
|
||||||
|
// - Lines ending with comma (BASIC line continuation)
|
||||||
|
// - Lines that are part of a continuation block (in_line_continuation is true)
|
||||||
|
if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}')
|
||||||
|
&& !upper.starts_with("SELECT ") && !upper.starts_with("CASE ") && upper != "END SELECT"
|
||||||
|
&& !ends_with_comma && !in_line_continuation {
|
||||||
result.push(';');
|
result.push(';');
|
||||||
}
|
}
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
|
|
||||||
|
// Update line continuation state
|
||||||
|
in_line_continuation = ends_with_comma;
|
||||||
} else {
|
} else {
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
|
|
@ -804,18 +1314,23 @@ impl ScriptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("[TOOL] IF/THEN conversion complete, output has {} lines", result.lines().count());
|
log::info!("[TOOL] IF/THEN conversion complete, output has {} lines", result.lines().count());
|
||||||
|
|
||||||
|
// Convert BASIC <> (not equal) to Rhai != globally
|
||||||
|
let result = result.replace(" <> ", " != ");
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert BASIC SELECT ... CASE / END SELECT to Rhai match expressions
|
/// Convert BASIC SELECT ... CASE / END SELECT to if-else chains
|
||||||
/// Transforms: SELECT var ... CASE "value" ... END SELECT
|
/// Transforms: SELECT var ... CASE "value" ... END SELECT
|
||||||
/// Into: match var { "value" => { ... } ... }
|
/// Into: if var == "value" { ... } else if var == "value2" { ... }
|
||||||
fn convert_select_case_syntax(script: &str) -> String {
|
/// Note: We use if-else instead of match because 'match' is a reserved keyword in Rhai
|
||||||
|
pub fn convert_select_case_syntax(script: &str) -> String {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
let mut lines: Vec<&str> = script.lines().collect();
|
let mut lines: Vec<&str> = script.lines().collect();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
||||||
log::info!("[TOOL] Converting SELECT/CASE syntax");
|
log::info!("[TOOL] Converting SELECT/CASE syntax to if-else chains");
|
||||||
|
|
||||||
while i < lines.len() {
|
while i < lines.len() {
|
||||||
let trimmed = lines[i].trim();
|
let trimmed = lines[i].trim();
|
||||||
|
|
@ -827,43 +1342,77 @@ impl ScriptService {
|
||||||
let select_var = trimmed[7..].trim(); // Skip "SELECT "
|
let select_var = trimmed[7..].trim(); // Skip "SELECT "
|
||||||
log::info!("[TOOL] Converting SELECT statement for variable: '{}'", select_var);
|
log::info!("[TOOL] Converting SELECT statement for variable: '{}'", select_var);
|
||||||
|
|
||||||
// Start match expression
|
|
||||||
result.push_str(&format!("match {} {{\n", select_var));
|
|
||||||
|
|
||||||
// Skip the SELECT line
|
// Skip the SELECT line
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
||||||
// Process CASE statements until END SELECT
|
// Process CASE statements until END SELECT
|
||||||
let mut current_case_body: Vec<String> = Vec::new();
|
let mut current_case_body: Vec<String> = Vec::new();
|
||||||
let mut in_case = false;
|
let mut in_case = false;
|
||||||
|
let mut is_first_case = true;
|
||||||
|
|
||||||
while i < lines.len() {
|
while i < lines.len() {
|
||||||
let case_trimmed = lines[i].trim();
|
let case_trimmed = lines[i].trim();
|
||||||
let case_upper = case_trimmed.to_uppercase();
|
let case_upper = case_trimmed.to_uppercase();
|
||||||
|
|
||||||
|
// Skip empty lines and comment lines within SELECT/CASE blocks
|
||||||
|
if case_trimmed.is_empty() || case_trimmed.starts_with('\'') || case_trimmed.starts_with('#') {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if case_upper == "END SELECT" {
|
if case_upper == "END SELECT" {
|
||||||
// Close any open case
|
// Close any open case
|
||||||
if in_case {
|
if in_case {
|
||||||
for body_line in ¤t_case_body {
|
for body_line in ¤t_case_body {
|
||||||
result.push_str(" ");
|
result.push_str(" ");
|
||||||
result.push_str(body_line);
|
result.push_str(body_line);
|
||||||
|
// Add semicolon if line doesn't have one
|
||||||
|
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
|
||||||
|
result.push(';');
|
||||||
|
}
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
|
// Close the last case arm (no else if, so we need the closing brace)
|
||||||
|
result.push_str(" }\n");
|
||||||
current_case_body.clear();
|
current_case_body.clear();
|
||||||
in_case = false;
|
in_case = false;
|
||||||
}
|
}
|
||||||
// Close the match expression
|
// No extra closing brace needed - the last } else if ... { already closed the chain
|
||||||
result.push_str("}\n");
|
|
||||||
i += 1;
|
i += 1;
|
||||||
break;
|
break;
|
||||||
} else if case_upper.starts_with("CASE ") {
|
} else if case_upper.starts_with("SELECT ") {
|
||||||
// Close previous case if any
|
// Encountered another SELECT statement while processing this SELECT block
|
||||||
|
// Close the current if-else chain and break to let the outer loop handle the new SELECT
|
||||||
if in_case {
|
if in_case {
|
||||||
for body_line in ¤t_case_body {
|
for body_line in ¤t_case_body {
|
||||||
result.push_str(" ");
|
result.push_str(" ");
|
||||||
result.push_str(body_line);
|
result.push_str(body_line);
|
||||||
|
// Add semicolon if line doesn't have one
|
||||||
|
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
|
||||||
|
result.push(';');
|
||||||
|
}
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
|
// Close the current case arm (no else if, so we need the closing brace)
|
||||||
|
result.push_str(" }\n");
|
||||||
|
current_case_body.clear();
|
||||||
|
in_case = false;
|
||||||
|
}
|
||||||
|
// No extra closing brace needed
|
||||||
|
break;
|
||||||
|
} else if case_upper.starts_with("CASE ") {
|
||||||
|
// Close previous case if any (but NOT if we're about to start else if)
|
||||||
|
if in_case {
|
||||||
|
for body_line in ¤t_case_body {
|
||||||
|
result.push_str(" ");
|
||||||
|
result.push_str(body_line);
|
||||||
|
// Add semicolon if line doesn't have one
|
||||||
|
if !body_line.ends_with(';') && !body_line.ends_with('{') && !body_line.ends_with('}') {
|
||||||
|
result.push(';');
|
||||||
|
}
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
// NOTE: Don't close the case arm here - the } else if will close it
|
||||||
current_case_body.clear();
|
current_case_body.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -876,14 +1425,22 @@ impl ScriptService {
|
||||||
format!("\"{}\"", case_trimmed[5..].trim())
|
format!("\"{}\"", case_trimmed[5..].trim())
|
||||||
};
|
};
|
||||||
|
|
||||||
result.push_str(&format!(" {} => {{\n", case_value));
|
// Start if/else if chain
|
||||||
|
if is_first_case {
|
||||||
|
result.push_str(&format!("if {} == {} {{\n", select_var, case_value));
|
||||||
|
is_first_case = false;
|
||||||
|
} else {
|
||||||
|
result.push_str(&format!("}} else if {} == {} {{\n", select_var, case_value));
|
||||||
|
}
|
||||||
in_case = true;
|
in_case = true;
|
||||||
i += 1;
|
i += 1;
|
||||||
} else {
|
} else if in_case {
|
||||||
// Collect body lines for the current case
|
// Collect body lines for the current case
|
||||||
if in_case {
|
|
||||||
current_case_body.push(lines[i].to_string());
|
current_case_body.push(lines[i].to_string());
|
||||||
}
|
i += 1;
|
||||||
|
} else {
|
||||||
|
// We're in the SELECT block but not in a CASE yet
|
||||||
|
// Skip this line and move to the next
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -892,17 +1449,19 @@ impl ScriptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a SELECT statement - just copy the line
|
// Not a SELECT statement - just copy the line
|
||||||
|
if i < lines.len() {
|
||||||
result.push_str(lines[i]);
|
result.push_str(lines[i]);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert BASIC keywords to lowercase without touching variables
|
/// Convert BASIC keywords to lowercase without touching variables
|
||||||
/// This is a simplified version of normalize_variables_to_lowercase for tools
|
/// This is a simplified version of normalize_variables_to_lowercase for tools
|
||||||
fn convert_keywords_to_lowercase(script: &str) -> String {
|
pub fn convert_keywords_to_lowercase(script: &str) -> String {
|
||||||
let keywords = [
|
let keywords = [
|
||||||
"IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND",
|
"IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND",
|
||||||
"DO", "LOOP", "RETURN", "EXIT",
|
"DO", "LOOP", "RETURN", "EXIT",
|
||||||
|
|
@ -1238,7 +1797,7 @@ impl ScriptService {
|
||||||
/// - "USE WEBSITE "url" REFRESH "interval"" → "USE_WEBSITE("url", "interval")"
|
/// - "USE WEBSITE "url" REFRESH "interval"" → "USE_WEBSITE("url", "interval")"
|
||||||
/// - "SET BOT MEMORY key AS value" → "SET_BOT_MEMORY(key, value)"
|
/// - "SET BOT MEMORY key AS value" → "SET_BOT_MEMORY(key, value)"
|
||||||
/// - "CLEAR SUGGESTIONS" → "CLEAR_SUGGESTIONS()"
|
/// - "CLEAR SUGGESTIONS" → "CLEAR_SUGGESTIONS()"
|
||||||
fn convert_multiword_keywords(script: &str) -> String {
|
pub fn convert_multiword_keywords(script: &str) -> String {
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
// Known multi-word keywords with their conversion patterns
|
// Known multi-word keywords with their conversion patterns
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue