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
657
src/basic/mod.rs
657
src/basic/mod.rs
|
|
@ -186,6 +186,9 @@ impl ScriptService {
|
|||
register_string_functions(state.clone(), user.clone(), &mut engine);
|
||||
switch_keyword(&state, 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);
|
||||
#[cfg(feature = "automation")]
|
||||
webhook_keyword(&state, user.clone(), &mut engine);
|
||||
|
|
@ -223,7 +226,6 @@ impl ScriptService {
|
|||
register_model_routing_keywords(state.clone(), user.clone(), &mut engine);
|
||||
register_multimodal_keywords(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
|
||||
|
|
@ -579,6 +581,7 @@ impl ScriptService {
|
|||
trimmed.starts_with("DESCRIPTION ") ||
|
||||
trimmed.starts_with("DESCRIPTION\t") ||
|
||||
trimmed.starts_with('\'') || // BASIC comment lines
|
||||
trimmed.starts_with('#') || // Hash comment lines
|
||||
trimmed.is_empty())
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
|
|
@ -589,10 +592,14 @@ impl ScriptService {
|
|||
// Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings)
|
||||
let script = preprocess_switch(&executable_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
|
||||
// Note: FORMAT is registered as a regular function, so FORMAT(expr, pattern) works directly
|
||||
|
||||
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 ... { }
|
||||
let script = Self::convert_if_then_syntax(&script);
|
||||
// Convert SELECT ... CASE / END SELECT to match expressions
|
||||
|
|
@ -619,32 +626,381 @@ impl ScriptService {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Convert FORMAT(expr, pattern) to FORMAT expr pattern (custom syntax format)
|
||||
/// 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 {
|
||||
use regex::Regex;
|
||||
let mut result = script.to_string();
|
||||
let mut result = String::new();
|
||||
let mut chars = script.chars().peekable();
|
||||
let mut i = 0;
|
||||
let bytes = script.as_bytes();
|
||||
|
||||
// First, process RANDOM to ensure commas are preserved
|
||||
// RANDOM(min, max) stays as RANDOM(min, max) - no conversion needed
|
||||
while i < bytes.len() {
|
||||
// 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
|
||||
// Need to handle nested functions carefully
|
||||
// Match FORMAT( ... ) but don't include inner function parentheses
|
||||
// This regex matches FORMAT followed by parentheses containing two comma-separated expressions
|
||||
if let Ok(re) = Regex::new(r"(?i)FORMAT\s*\(([^()]+(?:\([^()]*\)[^()]*)*),([^)]+)\)") {
|
||||
result = re.replace_all(&result, "FORMAT $1$2").to_string();
|
||||
// Find the arguments by tracking parentheses
|
||||
while j < bytes.len() && paren_depth > 0 {
|
||||
match bytes[j] {
|
||||
b'(' => paren_depth += 1,
|
||||
b')' => {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn convert_if_then_syntax(script: &str) -> String {
|
||||
pub fn convert_if_then_syntax(script: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut if_stack: Vec<bool> = Vec::new();
|
||||
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());
|
||||
|
||||
|
|
@ -653,7 +1009,7 @@ impl ScriptService {
|
|||
let upper = trimmed.to_uppercase();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
@ -666,6 +1022,19 @@ impl ScriptService {
|
|||
let condition = &trimmed[3..then_pos].trim();
|
||||
// Convert BASIC "NOT IN" to Rhai "!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);
|
||||
result.push_str("if ");
|
||||
result.push_str(&condition);
|
||||
|
|
@ -681,6 +1050,31 @@ impl ScriptService {
|
|||
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
|
||||
if upper == "END IF" {
|
||||
log::info!("[TOOL] Converting END IF statement");
|
||||
|
|
@ -709,6 +1103,85 @@ impl ScriptService {
|
|||
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)
|
||||
if in_with_block {
|
||||
// Check if this is a property assignment (identifier = value)
|
||||
|
|
@ -728,21 +1201,32 @@ impl ScriptService {
|
|||
result.push_str(" ");
|
||||
}
|
||||
|
||||
// Handle SAVE table, object → INSERT table, object
|
||||
// BASIC SAVE uses 2 parameters but Rhai SAVE needs 3
|
||||
// INSERT uses 2 parameters which matches the BASIC syntax
|
||||
// Handle SAVE table, field1, field2, ... → INSERT "table", #{field1: value1, field2: value2, ...}
|
||||
if upper.starts_with("SAVE") && upper.contains(',') {
|
||||
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 parts: Vec<&str> = after_save.split(',').collect();
|
||||
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 object_name = parts[1].trim().trim_end_matches(';');
|
||||
// Convert to INSERT table, object
|
||||
let converted = format!("INSERT \"{}\", {};\n", table, object_name);
|
||||
log::info!("[TOOL] Converted SAVE to INSERT: '{}'", converted);
|
||||
|
||||
// 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 converted = format!("INSERT \"{}\", {};\n", table, object_name);
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -787,16 +1271,42 @@ impl ScriptService {
|
|||
&& !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 {
|
||||
// Add 'let' for variable declarations
|
||||
result.push_str("let ");
|
||||
// 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(trimmed);
|
||||
result.push_str(&line_to_process);
|
||||
// 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('\n');
|
||||
|
||||
// Update line continuation state
|
||||
in_line_continuation = ends_with_comma;
|
||||
} else {
|
||||
result.push_str(trimmed);
|
||||
result.push('\n');
|
||||
|
|
@ -804,18 +1314,23 @@ impl ScriptService {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// Into: match var { "value" => { ... } ... }
|
||||
fn convert_select_case_syntax(script: &str) -> String {
|
||||
/// Into: if var == "value" { ... } else if var == "value2" { ... }
|
||||
/// 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 lines: Vec<&str> = script.lines().collect();
|
||||
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() {
|
||||
let trimmed = lines[i].trim();
|
||||
|
|
@ -827,43 +1342,77 @@ impl ScriptService {
|
|||
let select_var = trimmed[7..].trim(); // Skip "SELECT "
|
||||
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
|
||||
i += 1;
|
||||
|
||||
// Process CASE statements until END SELECT
|
||||
let mut current_case_body: Vec<String> = Vec::new();
|
||||
let mut in_case = false;
|
||||
let mut is_first_case = true;
|
||||
|
||||
while i < lines.len() {
|
||||
let case_trimmed = lines[i].trim();
|
||||
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" {
|
||||
// Close any open case
|
||||
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');
|
||||
}
|
||||
// Close the last case arm (no else if, so we need the closing brace)
|
||||
result.push_str(" }\n");
|
||||
current_case_body.clear();
|
||||
in_case = false;
|
||||
}
|
||||
// Close the match expression
|
||||
result.push_str("}\n");
|
||||
// No extra closing brace needed - the last } else if ... { already closed the chain
|
||||
i += 1;
|
||||
break;
|
||||
} else if case_upper.starts_with("CASE ") {
|
||||
// Close previous case if any
|
||||
} else if case_upper.starts_with("SELECT ") {
|
||||
// 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 {
|
||||
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');
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
|
@ -876,14 +1425,22 @@ impl ScriptService {
|
|||
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;
|
||||
i += 1;
|
||||
} else {
|
||||
} else if in_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;
|
||||
}
|
||||
}
|
||||
|
|
@ -892,9 +1449,11 @@ impl ScriptService {
|
|||
}
|
||||
|
||||
// Not a SELECT statement - just copy the line
|
||||
result.push_str(lines[i]);
|
||||
result.push('\n');
|
||||
i += 1;
|
||||
if i < lines.len() {
|
||||
result.push_str(lines[i]);
|
||||
result.push('\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
|
|
@ -902,7 +1461,7 @@ impl ScriptService {
|
|||
|
||||
/// Convert BASIC keywords to lowercase without touching variables
|
||||
/// 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 = [
|
||||
"IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND",
|
||||
"DO", "LOOP", "RETURN", "EXIT",
|
||||
|
|
@ -1238,7 +1797,7 @@ impl ScriptService {
|
|||
/// - "USE WEBSITE "url" REFRESH "interval"" → "USE_WEBSITE("url", "interval")"
|
||||
/// - "SET BOT MEMORY key AS value" → "SET_BOT_MEMORY(key, value)"
|
||||
/// - "CLEAR SUGGESTIONS" → "CLEAR_SUGGESTIONS()"
|
||||
fn convert_multiword_keywords(script: &str) -> String {
|
||||
pub fn convert_multiword_keywords(script: &str) -> String {
|
||||
use regex::Regex;
|
||||
|
||||
// Known multi-word keywords with their conversion patterns
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue