diff --git a/TestPrograms/transpiler_example.wfl b/TestPrograms/transpiler_example.wfl new file mode 100644 index 00000000..361224e0 --- /dev/null +++ b/TestPrograms/transpiler_example.wfl @@ -0,0 +1,76 @@ +// Example WFL program to demonstrate the JavaScript transpiler +// Run: wfl --transpile TestPrograms/transpiler_example.wfl + +display "=== WFL to JavaScript Transpiler Demo ===" +display "" + +// Variable declarations +store greeting as "Hello" +store counter as 0 +store is_active as yes + +// Action (function) definition +define action called add_numbers with parameters a and b: + return a plus b +end action + +// Action with display +define action called greet_user with parameter name: + display "Welcome, " with name with "!" +end action + +// Main entry point +define action called main: + display "Starting program..." + + // Using variables + display "Greeting: " with greeting + display "Counter: " with counter + display "Active: " with is_active + + // Calling actions + store result as add_numbers with 5 and 10 + display "5 + 10 = " with result + + greet_user with "Alice" + + // Conditional logic + check if is_active: + display "System is active" + otherwise: + display "System is inactive" + end check + + // Count loop + display "Counting from 1 to 5:" + count from 1 to 5: + display " Count: " with count + end count + + // List operations + create list colors: + add "red" + add "green" + add "blue" + end list + + display "Colors:" + for each color in colors: + display " - " with color + end for + + // Nested conditions + store score as 85 + check if score is greater than 90: + display "Grade: A" + otherwise: + check if score is greater than 80: + display "Grade: B" + otherwise: + display "Grade: C" + end check + end check + + display "" + display "=== Demo Complete ===" +end action diff --git a/src/lib.rs b/src/lib.rs index cc394964..895634bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,7 @@ pub mod parser; pub mod pattern; pub mod repl; pub mod stdlib; +pub mod transpiler; pub mod typechecker; pub mod version; pub mod wfl_config; diff --git a/src/main.rs b/src/main.rs index 7c2591f7..30e03207 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use wfl::lexer::lex_wfl_with_positions; use wfl::linter::Linter; use wfl::parser::Parser; use wfl::repl; +use wfl::transpiler::{TranspilerConfig, TranspilerTarget}; use wfl::typechecker::TypeChecker; use wfl::wfl_config; use wfl::{error, exec_trace, info}; @@ -41,6 +42,13 @@ fn print_help() { println!(" --time Measure and display execution time"); println!(" --test Run file in test mode"); println!(); + println!("TRANSPILATION:"); + println!(" --transpile Transpile WFL code to JavaScript"); + println!(" --target Target environment: node (default), browser, universal"); + println!(" --output Output file (default: .js)"); + println!(" --no-runtime Don't include WFL runtime in output"); + println!(" --es-modules Generate ES modules (export/import)"); + println!(); println!("Configuration Maintenance:"); println!(" --configCheck Check configuration files for issues"); println!(" --configFix Check and fix configuration files"); @@ -98,6 +106,10 @@ async fn main() -> io::Result<()> { let mut dump_env_mode = false; let mut output_path = None; let mut time_mode = false; + let mut transpile_mode = false; + let mut transpile_target = TranspilerTarget::Node; + let mut transpile_no_runtime = false; + let mut transpile_es_modules = false; let mut test_mode = false; let mut file_path = String::new(); @@ -276,6 +288,64 @@ async fn main() -> io::Result<()> { time_mode = true; i += 1; } + "--transpile" => { + if lint_mode || analyze_mode || fix_mode || config_check_mode || config_fix_mode { + eprintln!( + "Error: --transpile cannot be combined with --lint, --analyze, --fix, --configCheck, or --configFix" + ); + process::exit(2); + } + transpile_mode = true; + i += 1; + // Parse transpile options + while i < args.len() && args[i].starts_with("--") { + match args[i].as_str() { + "--target" => { + if i + 1 < args.len() { + transpile_target = match args[i + 1].as_str() { + "node" => TranspilerTarget::Node, + "browser" => TranspilerTarget::Browser, + "universal" => TranspilerTarget::Universal, + _ => { + eprintln!( + "Error: Unknown transpile target '{}'. Use: node, browser, or universal", + args[i + 1] + ); + process::exit(2); + } + }; + i += 2; + } else { + eprintln!("Error: --target requires an argument"); + process::exit(2); + } + } + "--no-runtime" => { + transpile_no_runtime = true; + i += 1; + } + "--es-modules" => { + transpile_es_modules = true; + i += 1; + } + "--output" => { + if i + 1 < args.len() && !args[i + 1].starts_with("--") { + output_path = Some(args[i + 1].clone()); + i += 2; + } else { + eprintln!("Error: --output requires a file path"); + process::exit(2); + } + } + _ => break, + } + } + // Get input file if not already set + if i < args.len() && !args[i].starts_with("--") && file_path.is_empty() { + file_path = args[i].clone(); + i += 1; + } + } "--test" => { if lint_mode || analyze_mode || fix_mode || config_check_mode || config_fix_mode { eprintln!( @@ -575,6 +645,79 @@ async fn main() -> io::Result<()> { } } + // Handle transpile mode + if transpile_mode { + let tokens_with_pos = lex_wfl_with_positions(&input); + match Parser::new(&tokens_with_pos).parse() { + Ok(program) => { + // Configure the transpiler + let transpiler_config = TranspilerConfig { + include_runtime: !transpile_no_runtime, + source_maps: false, + target: transpile_target, + minify: false, + indent: " ".to_string(), + es_modules: transpile_es_modules, + }; + + // Run the transpiler + match wfl::transpiler::transpile(&program, &transpiler_config) { + Ok(result) => { + // Show warnings if any + for warning in &result.warnings { + eprintln!( + "Warning at line {}, column {}: {}", + warning.line, warning.column, warning.message + ); + } + + // Determine output path + let output_file = output_path.unwrap_or_else(|| { + let base = Path::new(&file_path); + let stem = base.file_stem().unwrap_or_default().to_string_lossy(); + format!("{}.js", stem) + }); + + // Write output + if let Err(e) = fs::write(&output_file, &result.code) { + eprintln!("Error writing output file: {e}"); + process::exit(1); + } + + println!("Transpiled to: {output_file}"); + if !result.warnings.is_empty() { + println!(" ({} warnings)", result.warnings.len()); + } + process::exit(0); + } + Err(e) => { + eprintln!( + "Transpilation error at line {}, column {}: {}", + e.line, e.column, e.message + ); + process::exit(1); + } + } + } + Err(errors) => { + eprintln!("Parse errors:"); + + let mut reporter = DiagnosticReporter::new(); + let file_id = reporter.add_file(&file_path, &input); + + for error in errors { + let diagnostic = reporter.convert_parse_error(file_id, &error); + if let Err(e) = reporter.report_diagnostic(file_id, &diagnostic) { + eprintln!("Error displaying diagnostic: {e}"); + eprintln!("Error: {error}"); + } + } + + process::exit(2); + } + } + } + if lint_mode { let tokens_with_pos = lex_wfl_with_positions(&input); match Parser::new(&tokens_with_pos).parse() { diff --git a/src/transpiler/javascript.rs b/src/transpiler/javascript.rs new file mode 100644 index 00000000..d769e628 --- /dev/null +++ b/src/transpiler/javascript.rs @@ -0,0 +1,1977 @@ +//! JavaScript Code Generator +//! +//! This module contains the JavaScript code generator that transforms +//! WFL AST nodes into JavaScript code. + +use crate::parser::ast::{ + Anchor, Argument, CharClass, ErrorType, Expression, FileOpenMode, Literal, Operator, Parameter, + PatternExpression, Program, Quantifier, Statement, UnaryOperator, WriteMode, +}; + +use super::runtime::get_runtime; +use super::{TranspileError, TranspileResult, TranspileWarning, TranspilerConfig}; + +/// JavaScript transpiler that converts WFL AST to JavaScript code +pub struct JavaScriptTranspiler { + config: TranspilerConfig, + indent_level: usize, + warnings: Vec, + /// Track whether we're in an async context + in_async: bool, +} + +impl JavaScriptTranspiler { + /// Create a new JavaScript transpiler with the given configuration + pub fn new(config: TranspilerConfig) -> Self { + Self { + config, + indent_level: 0, + warnings: Vec::new(), + in_async: false, + } + } + + /// Get the current indentation string + fn indent(&self) -> String { + self.config.indent.repeat(self.indent_level) + } + + /// Increase indentation level + fn push_indent(&mut self) { + self.indent_level += 1; + } + + /// Decrease indentation level + fn pop_indent(&mut self) { + if self.indent_level > 0 { + self.indent_level -= 1; + } + } + + /// Add a warning + fn warn(&mut self, message: impl Into, line: usize, column: usize) { + self.warnings.push(TranspileWarning { + message: message.into(), + line, + column, + }); + } + + /// Transpile a WFL program to JavaScript + pub fn transpile(mut self, program: &Program) -> Result { + let mut output = String::new(); + + // Add runtime if configured + if self.config.include_runtime { + output.push_str(get_runtime(self.config.target)); + output.push_str("\n\n"); + } + + // Wrap in IIFE if not using ES modules + if !self.config.es_modules { + output.push_str("(function() {\n"); + output.push_str("'use strict';\n\n"); + self.push_indent(); + } + + // First pass: collect all action definitions to hoist them + let (actions, other_stmts): (Vec<_>, Vec<_>) = program + .statements + .iter() + .partition(|s| matches!(s, Statement::ActionDefinition { .. })); + + // Generate action definitions first (hoisted) + for stmt in &actions { + let code = self.transpile_statement(stmt)?; + output.push_str(&code); + output.push('\n'); + } + + // Generate other statements + for stmt in &other_stmts { + let code = self.transpile_statement(stmt)?; + output.push_str(&code); + output.push('\n'); + } + + // Check for main action and call it + let main_action = actions + .iter() + .find(|s| matches!(s, Statement::ActionDefinition { name, .. } if name == "main")); + + if let Some(Statement::ActionDefinition { body, .. }) = main_action { + let is_main_async = self.contains_async(body); + output.push_str(&self.indent()); + output.push_str("// Entry point\n"); + output.push_str(&self.indent()); + if is_main_async { + output.push_str("(async () => { await main(); })();\n"); + } else { + output.push_str("main();\n"); + } + } + + // Close IIFE + if !self.config.es_modules { + self.pop_indent(); + output.push_str("})();\n"); + } + + Ok(TranspileResult { + code: output, + warnings: self.warnings, + }) + } + + /// Transpile a statement to JavaScript + fn transpile_statement(&mut self, stmt: &Statement) -> Result { + match stmt { + Statement::VariableDeclaration { + name, + value, + is_constant, + .. + } => { + let keyword = if *is_constant { "const" } else { "let" }; + let val = self.transpile_expression(value)?; + Ok(format!( + "{}{} {} = {};\n", + self.indent(), + keyword, + self.sanitize_name(name), + val + )) + } + + Statement::Assignment { name, value, .. } => { + let val = self.transpile_expression(value)?; + Ok(format!( + "{}{} = {};\n", + self.indent(), + self.sanitize_name(name), + val + )) + } + + Statement::IfStatement { + condition, + then_block, + else_block, + .. + } => { + let cond = self.transpile_expression(condition)?; + let mut result = format!("{}if ({}) {{\n", self.indent(), cond); + self.push_indent(); + for s in then_block { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + if let Some(else_stmts) = else_block { + result.push_str(&format!("{}}} else {{\n", self.indent())); + self.push_indent(); + for s in else_stmts { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + } + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::SingleLineIf { + condition, + then_stmt, + else_stmt, + .. + } => { + let cond = self.transpile_expression(condition)?; + let then_code = self.transpile_statement(then_stmt)?; + let mut result = + format!("{}if ({}) {}", self.indent(), cond, then_code.trim_start()); + if let Some(else_s) = else_stmt { + let else_code = self.transpile_statement(else_s)?; + result = format!( + "{}if ({}) {{ {} }} else {{ {} }}\n", + self.indent(), + cond, + then_code.trim(), + else_code.trim() + ); + } + Ok(result) + } + + Statement::ForEachLoop { + item_name, + collection, + reversed, + body, + .. + } => { + let coll = self.transpile_expression(collection)?; + let iter_var = self.sanitize_name(item_name); + let mut result = if *reversed { + format!( + "{}for (const {} of [...{}].reverse()) {{\n", + self.indent(), + iter_var, + coll + ) + } else { + format!("{}for (const {} of {}) {{\n", self.indent(), iter_var, coll) + }; + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::CountLoop { + start, + end, + step, + downward, + variable_name, + body, + .. + } => { + let start_val = self.transpile_expression(start)?; + let end_val = self.transpile_expression(end)?; + let var_name = variable_name.as_deref().unwrap_or("count"); + let var_name = self.sanitize_name(var_name); + let step_val = step + .as_ref() + .map(|s| self.transpile_expression(s)) + .transpose()? + .unwrap_or_else(|| "1".to_string()); + + let (compare_op, update_op) = if *downward { + (">=", "-=") + } else { + ("<=", "+=") + }; + + let mut result = format!( + "{}for (let {} = {}; {} {} {}; {} {} {}) {{\n", + self.indent(), + var_name, + start_val, + var_name, + compare_op, + end_val, + var_name, + update_op, + step_val + ); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::WhileLoop { + condition, body, .. + } => { + let cond = self.transpile_expression(condition)?; + let mut result = format!("{}while ({}) {{\n", self.indent(), cond); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::RepeatWhileLoop { + condition, body, .. + } => { + let cond = self.transpile_expression(condition)?; + let mut result = format!("{}do {{\n", self.indent()); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}} while ({});\n", self.indent(), cond)); + Ok(result) + } + + Statement::RepeatUntilLoop { + condition, body, .. + } => { + let cond = self.transpile_expression(condition)?; + let mut result = format!("{}do {{\n", self.indent()); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}} while (!({}));\n", self.indent(), cond)); + Ok(result) + } + + Statement::ForeverLoop { body, .. } => { + let mut result = format!("{}while (true) {{\n", self.indent()); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::MainLoop { body, .. } => { + // Main loop is essentially a forever loop + let mut result = format!("{}while (true) {{\n", self.indent()); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::DisplayStatement { value, .. } => { + let val = self.transpile_expression(value)?; + Ok(format!("{}WFL.display({});\n", self.indent(), val)) + } + + Statement::ActionDefinition { + name, + parameters, + body, + .. + } => { + // Check if the body contains any async operations + let is_async = self.contains_async(body); + let old_async = self.in_async; + self.in_async = is_async; + + let params = parameters + .iter() + .map(|p| self.transpile_parameter(p)) + .collect::, _>>()? + .join(", "); + + let async_keyword = if is_async { "async " } else { "" }; + let mut result = format!( + "{}{}function {}({}) {{\n", + self.indent(), + async_keyword, + self.sanitize_name(name), + params + ); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + + self.in_async = old_async; + Ok(result) + } + + Statement::ReturnStatement { value, .. } => { + if let Some(val) = value { + let v = self.transpile_expression(val)?; + Ok(format!("{}return {};\n", self.indent(), v)) + } else { + Ok(format!("{}return;\n", self.indent())) + } + } + + Statement::ExpressionStatement { expression, .. } => { + let expr = self.transpile_expression(expression)?; + Ok(format!("{}{};\n", self.indent(), expr)) + } + + Statement::BreakStatement { .. } => Ok(format!("{}break;\n", self.indent())), + + Statement::ContinueStatement { .. } => Ok(format!("{}continue;\n", self.indent())), + + Statement::ExitStatement { .. } => Ok(format!("{}WFL.exit(0);\n", self.indent())), + + Statement::CreateListStatement { + name, + initial_values, + .. + } => { + let values = initial_values + .iter() + .map(|e| self.transpile_expression(e)) + .collect::, _>>()? + .join(", "); + Ok(format!( + "{}let {} = [{}];\n", + self.indent(), + self.sanitize_name(name), + values + )) + } + + Statement::PushStatement { list, value, .. } => { + let list_expr = self.transpile_expression(list)?; + let val = self.transpile_expression(value)?; + Ok(format!("{}{}.push({});\n", self.indent(), list_expr, val)) + } + + Statement::AddToListStatement { + value, list_name, .. + } => { + let val = self.transpile_expression(value)?; + Ok(format!( + "{}{}.push({});\n", + self.indent(), + self.sanitize_name(list_name), + val + )) + } + + Statement::RemoveFromListStatement { + value, list_name, .. + } => { + let val = self.transpile_expression(value)?; + Ok(format!( + "{}WFL.list.remove({}, {});\n", + self.indent(), + self.sanitize_name(list_name), + val + )) + } + + Statement::ClearListStatement { list_name, .. } => Ok(format!( + "{}{}.length = 0;\n", + self.indent(), + self.sanitize_name(list_name) + )), + + Statement::MapCreation { name, entries, .. } => { + let mut result = + format!("{}let {} = {{\n", self.indent(), self.sanitize_name(name)); + self.push_indent(); + for (key, value) in entries { + let val = self.transpile_expression(value)?; + result.push_str(&format!( + "{}{}: {},\n", + self.indent(), + self.escape_key(key), + val + )); + } + self.pop_indent(); + result.push_str(&format!("{}}};\n", self.indent())); + Ok(result) + } + + Statement::ReadFileStatement { + path, + variable_name, + .. + } => { + let path_expr = self.transpile_expression(path)?; + Ok(format!( + "{}let {} = WFL.file.read({});\n", + self.indent(), + self.sanitize_name(variable_name), + path_expr + )) + } + + Statement::WriteFileStatement { + file, + content, + mode, + .. + } => { + let file_expr = self.transpile_expression(file)?; + let content_expr = self.transpile_expression(content)?; + let func = match mode { + WriteMode::Overwrite => "write", + WriteMode::Append => "append", + }; + Ok(format!( + "{}WFL.file.{}({}, {});\n", + self.indent(), + func, + file_expr, + content_expr + )) + } + + Statement::CreateFileStatement { path, content, .. } => { + let path_expr = self.transpile_expression(path)?; + let content_expr = self.transpile_expression(content)?; + Ok(format!( + "{}WFL.file.write({}, {});\n", + self.indent(), + path_expr, + content_expr + )) + } + + Statement::DeleteFileStatement { path, .. } => { + let path_expr = self.transpile_expression(path)?; + Ok(format!( + "{}WFL.file.delete({});\n", + self.indent(), + path_expr + )) + } + + Statement::CreateDirectoryStatement { path, .. } => { + let path_expr = self.transpile_expression(path)?; + Ok(format!( + "{}WFL.directory.create({});\n", + self.indent(), + path_expr + )) + } + + Statement::DeleteDirectoryStatement { path, .. } => { + let path_expr = self.transpile_expression(path)?; + Ok(format!( + "{}WFL.directory.delete({});\n", + self.indent(), + path_expr + )) + } + + Statement::OpenFileStatement { + path, + variable_name, + mode, + .. + } => { + let path_expr = self.transpile_expression(path)?; + let mode_str = match mode { + FileOpenMode::Read => "r", + FileOpenMode::Write => "w", + FileOpenMode::Append => "a", + }; + Ok(format!( + "{}let {} = {{ path: {}, mode: '{}', content: null }};\n", + self.indent(), + self.sanitize_name(variable_name), + path_expr, + mode_str + )) + } + + Statement::WriteToStatement { content, file, .. } => { + let content_expr = self.transpile_expression(content)?; + let file_expr = self.transpile_expression(file)?; + Ok(format!( + "{}WFL.file.write({}.path, {});\n", + self.indent(), + file_expr, + content_expr + )) + } + + Statement::CloseFileStatement { .. } => { + // JavaScript doesn't need explicit file closing for sync operations + Ok(format!("{}// File closed (no-op in JS)\n", self.indent())) + } + + Statement::WriteContentStatement { + content, target, .. + } => { + let content_expr = self.transpile_expression(content)?; + let target_expr = self.transpile_expression(target)?; + Ok(format!( + "{}WFL.file.write({}, {});\n", + self.indent(), + target_expr, + content_expr + )) + } + + Statement::ExecuteCommandStatement { + command, + arguments, + variable_name, + .. + } => { + let cmd = self.transpile_expression(command)?; + let args = arguments + .as_ref() + .map(|a| self.transpile_expression(a)) + .transpose()?; + let exec_call = if let Some(a) = args { + format!("WFL.process.execute({}, {})", cmd, a) + } else { + format!("WFL.process.execute({})", cmd) + }; + if let Some(var) = variable_name { + Ok(format!( + "{}let {} = {};\n", + self.indent(), + self.sanitize_name(var), + exec_call + )) + } else { + Ok(format!("{}{};\n", self.indent(), exec_call)) + } + } + + Statement::SpawnProcessStatement { + command, + arguments, + variable_name, + .. + } => { + let cmd = self.transpile_expression(command)?; + let args = arguments + .as_ref() + .map(|a| self.transpile_expression(a)) + .transpose()?; + let spawn_call = if let Some(a) = args { + format!("WFL.process.spawn({}, {})", cmd, a) + } else { + format!("WFL.process.spawn({})", cmd) + }; + Ok(format!( + "{}let {} = {};\n", + self.indent(), + self.sanitize_name(variable_name), + spawn_call + )) + } + + Statement::KillProcessStatement { process_id, .. } => { + let pid = self.transpile_expression(process_id)?; + Ok(format!("{}WFL.process.kill({});\n", self.indent(), pid)) + } + + Statement::ReadProcessOutputStatement { + process_id, + variable_name, + .. + } => { + let pid = self.transpile_expression(process_id)?; + Ok(format!( + "{}let {} = {}.stdout;\n", + self.indent(), + self.sanitize_name(variable_name), + pid + )) + } + + Statement::WaitForProcessStatement { + process_id, + variable_name, + .. + } => { + let pid = self.transpile_expression(process_id)?; + if let Some(var) = variable_name { + Ok(format!( + "{}let {} = await {}.exitCode;\n", + self.indent(), + self.sanitize_name(var), + pid + )) + } else { + Ok(format!("{}await {}.exitCode;\n", self.indent(), pid)) + } + } + + Statement::WaitForStatement { inner, .. } => { + // For variable declarations, we need to await the expression part, + // not the entire statement + match inner.as_ref() { + Statement::VariableDeclaration { + name, + value, + is_constant, + .. + } => { + let js_name = self.sanitize_name(name); + let awaited_value = format!("await {}", self.transpile_expression(value)?); + let keyword = if *is_constant { "const" } else { "let" }; + Ok(format!( + "{}{} {} = {};\n", + self.indent(), + keyword, + js_name, + awaited_value + )) + } + Statement::Assignment { name, value, .. } => { + let js_name = self.sanitize_name(name); + let awaited_value = format!("await {}", self.transpile_expression(value)?); + Ok(format!( + "{}{} = {};\n", + self.indent(), + js_name, + awaited_value + )) + } + _ => { + // For other statement types, wrap the result in await + let inner_code = self.transpile_statement(inner)?; + let trimmed = inner_code.trim(); + if let Some(expr) = trimmed.strip_suffix(';') { + Ok(format!("{}await {};\n", self.indent(), expr.trim_start())) + } else { + Ok(format!("{}await {};\n", self.indent(), trimmed)) + } + } + } + } + + Statement::WaitForDurationStatement { duration, unit, .. } => { + let dur = self.transpile_expression(duration)?; + let ms = match unit.as_str() { + "milliseconds" | "ms" => dur, + "seconds" | "s" => format!("({}) * 1000", dur), + "minutes" | "m" => format!("({}) * 60000", dur), + "hours" | "h" => format!("({}) * 3600000", dur), + _ => dur, + }; + Ok(format!("{}await WFL.sleep({});\n", self.indent(), ms)) + } + + Statement::HttpGetStatement { + url, variable_name, .. + } => { + let url_expr = self.transpile_expression(url)?; + Ok(format!( + "{}let {} = await WFL.http.get({});\n", + self.indent(), + self.sanitize_name(variable_name), + url_expr + )) + } + + Statement::HttpPostStatement { + url, + data, + variable_name, + .. + } => { + let url_expr = self.transpile_expression(url)?; + let data_expr = self.transpile_expression(data)?; + Ok(format!( + "{}let {} = await WFL.http.post({}, {});\n", + self.indent(), + self.sanitize_name(variable_name), + url_expr, + data_expr + )) + } + + Statement::TryStatement { + body, + when_clauses, + otherwise_block, + .. + } => { + let mut result = format!("{}try {{\n", self.indent()); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}} catch (_wfl_error) {{\n", self.indent())); + self.push_indent(); + + // Generate when clauses as if-else chain + let mut first = true; + for clause in when_clauses { + let error_check = match clause.error_type { + ErrorType::General => "true".to_string(), + ErrorType::FileNotFound => "_wfl_error.code === 'ENOENT'".to_string(), + ErrorType::PermissionDenied => "_wfl_error.code === 'EACCES'".to_string(), + _ => "true".to_string(), + }; + + let keyword = if first { "if" } else { "else if" }; + first = false; + result.push_str(&format!( + "{}{} ({}) {{\n", + self.indent(), + keyword, + error_check + )); + self.push_indent(); + // Bind the error name + result.push_str(&format!( + "{}let {} = _wfl_error;\n", + self.indent(), + self.sanitize_name(&clause.error_name) + )); + for s in &clause.body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}", self.indent())); + } + + if let Some(otherwise) = otherwise_block { + if first { + // No when clauses, just execute otherwise + for s in otherwise { + result.push_str(&self.transpile_statement(s)?); + } + } else { + result.push_str(" else {\n"); + self.push_indent(); + for s in otherwise { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}", self.indent())); + } + } + result.push('\n'); + + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::ContainerDefinition { + name, + extends, + properties, + methods, + static_properties, + static_methods, + .. + } => { + let extends_clause = extends + .as_ref() + .map(|e| format!(" extends {}", e)) + .unwrap_or_default(); + + let mut result = format!( + "{}class {}{} {{\n", + self.indent(), + self.sanitize_name(name), + extends_clause + ); + self.push_indent(); + + // Constructor + result.push_str(&format!("{}constructor() {{\n", self.indent())); + self.push_indent(); + if extends.is_some() { + result.push_str(&format!("{}super();\n", self.indent())); + } + result.push_str(&format!("{}this._wfl_container = true;\n", self.indent())); + result.push_str(&format!("{}this._wfl_type = '{}';\n", self.indent(), name)); + + // Initialize properties + for prop in properties { + let default_val = prop + .default_value + .as_ref() + .map(|v| self.transpile_expression(v)) + .transpose()? + .unwrap_or_else(|| "null".to_string()); + result.push_str(&format!( + "{}this.{} = {};\n", + self.indent(), + self.sanitize_name(&prop.name), + default_val + )); + } + + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + + // Static properties + for prop in static_properties { + let default_val = prop + .default_value + .as_ref() + .map(|v| self.transpile_expression(v)) + .transpose()? + .unwrap_or_else(|| "null".to_string()); + result.push_str(&format!( + "{}static {} = {};\n", + self.indent(), + self.sanitize_name(&prop.name), + default_val + )); + } + + // Methods + for method in methods { + if let Statement::ActionDefinition { + name: method_name, + parameters, + body, + .. + } = method + { + let is_async = self.contains_async(body); + let async_keyword = if is_async { "async " } else { "" }; + let params = parameters + .iter() + .map(|p| self.transpile_parameter(p)) + .collect::, _>>()? + .join(", "); + + result.push_str(&format!( + "{}{}{}({}) {{\n", + self.indent(), + async_keyword, + self.sanitize_name(method_name), + params + )); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + } + } + + // Static methods + for method in static_methods { + if let Statement::ActionDefinition { + name: method_name, + parameters, + body, + .. + } = method + { + let is_async = self.contains_async(body); + let async_keyword = if is_async { "async " } else { "" }; + let params = parameters + .iter() + .map(|p| self.transpile_parameter(p)) + .collect::, _>>()? + .join(", "); + + result.push_str(&format!( + "{}static {}{}({}) {{\n", + self.indent(), + async_keyword, + self.sanitize_name(method_name), + params + )); + self.push_indent(); + for s in body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + } + } + + self.pop_indent(); + result.push_str(&format!("{}}}\n", self.indent())); + Ok(result) + } + + Statement::ContainerInstantiation { + container_type, + instance_name, + property_initializers, + .. + } => { + let mut result = format!( + "{}let {} = new {}();\n", + self.indent(), + self.sanitize_name(instance_name), + self.sanitize_name(container_type) + ); + for init in property_initializers { + let val = self.transpile_expression(&init.value)?; + result.push_str(&format!( + "{}{}.{} = {};\n", + self.indent(), + self.sanitize_name(instance_name), + self.sanitize_name(&init.name), + val + )); + } + Ok(result) + } + + Statement::InterfaceDefinition { + name, line, column, .. + } => { + // Interfaces don't exist in JavaScript, emit a comment + self.warn( + format!("Interface '{}' has no JavaScript equivalent, skipped", name), + *line, + *column, + ); + Ok(format!( + "{}// Interface {} (no JS equivalent)\n", + self.indent(), + name + )) + } + + Statement::PatternDefinition { name, pattern, .. } => { + let pattern_regex = self.transpile_pattern_expression(pattern)?; + Ok(format!( + "{}const {} = new WFL.Pattern({});\n", + self.indent(), + self.sanitize_name(name), + pattern_regex + )) + } + + Statement::CreateDateStatement { name, value, .. } => { + let date_expr = if let Some(v) = value { + let val = self.transpile_expression(v)?; + format!("new Date({})", val) + } else { + "WFL.time.today()".to_string() + }; + Ok(format!( + "{}let {} = {};\n", + self.indent(), + self.sanitize_name(name), + date_expr + )) + } + + Statement::CreateTimeStatement { name, value, .. } => { + let time_expr = if let Some(v) = value { + let val = self.transpile_expression(v)?; + format!("new Date({})", val) + } else { + "new Date()".to_string() + }; + Ok(format!( + "{}let {} = {};\n", + self.indent(), + self.sanitize_name(name), + time_expr + )) + } + + Statement::EventDefinition { + name, line, column, .. + } => { + self.warn( + format!("Event '{}' definition not fully supported in JS", name), + *line, + *column, + ); + Ok(format!("{}// Event: {}\n", self.indent(), name)) + } + + Statement::EventTrigger { + name, arguments, .. + } => { + let args = arguments + .iter() + .map(|a| self.transpile_argument(a)) + .collect::, _>>()? + .join(", "); + Ok(format!( + "{}this.dispatchEvent(new CustomEvent('{}', {{ detail: [{}] }}));\n", + self.indent(), + name, + args + )) + } + + Statement::EventHandler { + event_source, + event_name, + handler_body, + .. + } => { + let source = self.transpile_expression(event_source)?; + let mut result = format!( + "{}{}.addEventListener('{}', (event) => {{\n", + self.indent(), + source, + event_name + ); + self.push_indent(); + for s in handler_body { + result.push_str(&self.transpile_statement(s)?); + } + self.pop_indent(); + result.push_str(&format!("{}}});\n", self.indent())); + Ok(result) + } + + Statement::ParentMethodCall { + method_name, + arguments, + .. + } => { + let args = arguments + .iter() + .map(|a| self.transpile_argument(a)) + .collect::, _>>()? + .join(", "); + Ok(format!( + "{}super.{}({});\n", + self.indent(), + self.sanitize_name(method_name), + args + )) + } + + Statement::LoadModuleStatement { + path, + alias, + line, + column, + .. + } => { + self.warn( + "Module loading requires bundler support in JavaScript", + *line, + *column, + ); + let path_expr = self.transpile_expression(path)?; + if let Some(a) = alias { + Ok(format!( + "{}const {} = require({});\n", + self.indent(), + self.sanitize_name(a), + path_expr + )) + } else { + Ok(format!("{}require({});\n", self.indent(), path_expr)) + } + } + + Statement::ListenStatement { + port, + server_name, + line, + column, + .. + } => { + self.warn( + "Server functionality has limitations: WFL's web server statements are transpiled to basic Node.js http setup. Features like middleware, routing, and request handling require manual implementation in JS", + *line, + *column, + ); + let port_expr = self.transpile_expression(port)?; + Ok(format!( + "{}// Note: Basic server setup only - implement request handlers manually\n{}const {} = require('http').createServer(); {}.listen({});\n", + self.indent(), + self.indent(), + self.sanitize_name(server_name), + self.sanitize_name(server_name), + port_expr + )) + } + + Statement::WaitForRequestStatement { line, column, .. } => { + self.warn( + "WaitForRequest cannot be directly transpiled: WFL's synchronous request waiting model doesn't map to JavaScript's event-driven model. Use server.on('request', callback) pattern instead", + *line, + *column, + ); + Ok(format!( + "{}// TODO: WaitForRequest - implement using server.on('request', (req, res) => {{ ... }}) pattern\n", + self.indent() + )) + } + + Statement::RespondStatement { + content, + status, + content_type, + line, + column, + .. + } => { + self.warn("Respond requires request context in JS", *line, *column); + let content_expr = self.transpile_expression(content)?; + let status_expr = status + .as_ref() + .map(|s| self.transpile_expression(s)) + .transpose()? + .unwrap_or_else(|| "200".to_string()); + let ct_expr = content_type + .as_ref() + .map(|c| self.transpile_expression(c)) + .transpose()? + .unwrap_or_else(|| "'text/html'".to_string()); + Ok(format!( + "{}// response.writeHead({}, {{ 'Content-Type': {} }}); response.end({});\n", + self.indent(), + status_expr, + ct_expr, + content_expr + )) + } + + Statement::RegisterSignalHandlerStatement { + signal_type, + handler_name, + .. + } => { + let signal = match signal_type.as_str() { + "SIGINT" => "SIGINT", + "SIGTERM" => "SIGTERM", + _ => signal_type, + }; + Ok(format!( + "{}process.on('{}', {});\n", + self.indent(), + signal, + self.sanitize_name(handler_name) + )) + } + + Statement::StopAcceptingConnectionsStatement { server, .. } => { + let server_expr = self.transpile_expression(server)?; + Ok(format!("{}{}.close();\n", self.indent(), server_expr)) + } + + Statement::CloseServerStatement { server, .. } => { + let server_expr = self.transpile_expression(server)?; + Ok(format!("{}{}.close();\n", self.indent(), server_expr)) + } + + // Testing statements - transpile to JavaScript test framework equivalents + Statement::DescribeBlock { + description, + setup, + teardown, + tests, + .. + } => { + let mut result = format!( + "{}describe({}, function() {{\n", + self.indent(), + self.escape_string(description) + ); + self.push_indent(); + // Transpile setup (beforeEach) + if let Some(setup_stmts) = setup { + result.push_str(&format!("{}beforeEach(function() {{\n", self.indent())); + self.push_indent(); + for stmt in setup_stmts { + result.push_str(&self.transpile_statement(stmt)?); + } + self.pop_indent(); + result.push_str(&format!("{}}});\n", self.indent())); + } + // Transpile teardown (afterEach) + if let Some(teardown_stmts) = teardown { + result.push_str(&format!("{}afterEach(function() {{\n", self.indent())); + self.push_indent(); + for stmt in teardown_stmts { + result.push_str(&self.transpile_statement(stmt)?); + } + self.pop_indent(); + result.push_str(&format!("{}}});\n", self.indent())); + } + // Transpile tests + for stmt in tests { + result.push_str(&self.transpile_statement(stmt)?); + } + self.pop_indent(); + result.push_str(&format!("{}}});\n", self.indent())); + Ok(result) + } + + Statement::TestBlock { + description, body, .. + } => { + let mut result = format!( + "{}it({}, function() {{\n", + self.indent(), + self.escape_string(description) + ); + self.push_indent(); + for stmt in body { + result.push_str(&self.transpile_statement(stmt)?); + } + self.pop_indent(); + result.push_str(&format!("{}}});\n", self.indent())); + Ok(result) + } + + Statement::ExpectStatement { + subject, assertion, .. + } => { + use crate::parser::ast::Assertion; + let subject_expr = self.transpile_expression(subject)?; + match assertion { + Assertion::Equal(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toEqual({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::Be(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toBe({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::GreaterThan(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toBeGreaterThan({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::LessThan(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toBeLessThan({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::BeYes => Ok(format!( + "{}expect({}).toBeTruthy();\n", + self.indent(), + subject_expr + )), + Assertion::BeNo => Ok(format!( + "{}expect({}).toBeFalsy();\n", + self.indent(), + subject_expr + )), + Assertion::Exist => Ok(format!( + "{}expect({}).toBeDefined();\n", + self.indent(), + subject_expr + )), + Assertion::Contain(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toContain({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::BeEmpty => Ok(format!( + "{}expect({}).toHaveLength(0);\n", + self.indent(), + subject_expr + )), + Assertion::HaveLength(expected) => { + let expected_expr = self.transpile_expression(expected)?; + Ok(format!( + "{}expect({}).toHaveLength({});\n", + self.indent(), + subject_expr, + expected_expr + )) + } + Assertion::BeOfType(type_name) => Ok(format!( + "{}expect(typeof {}).toBe({});\n", + self.indent(), + subject_expr, + self.escape_string(type_name) + )), + } + } + } + } + + /// Transpile an expression to JavaScript + fn transpile_expression(&mut self, expr: &Expression) -> Result { + match expr { + Expression::Literal(lit, _, _) => self.transpile_literal(lit), + + Expression::Variable(name, _, _) => Ok(self.sanitize_name(name)), + + Expression::BinaryOperation { + left, + operator, + right, + .. + } => { + let l = self.transpile_expression(left)?; + let r = self.transpile_expression(right)?; + let op = self.transpile_operator(operator); + // Handle special case for Contains + if *operator == Operator::Contains { + Ok(format!("({}).includes({})", l, r)) + } else { + Ok(format!("({} {} {})", l, op, r)) + } + } + + Expression::UnaryOperation { + operator, + expression, + .. + } => { + let expr = self.transpile_expression(expression)?; + let op = match operator { + UnaryOperator::Not => "!", + UnaryOperator::Minus => "-", + }; + Ok(format!("({}{})", op, expr)) + } + + Expression::FunctionCall { + function, + arguments, + .. + } => { + let func = self.transpile_expression(function)?; + let args = arguments + .iter() + .map(|a| self.transpile_argument(a)) + .collect::, _>>()? + .join(", "); + Ok(format!("{}({})", func, args)) + } + + Expression::ActionCall { + name, arguments, .. + } => { + let args = arguments + .iter() + .map(|a| self.transpile_argument(a)) + .collect::, _>>()? + .join(", "); + Ok(format!("{}({})", self.sanitize_name(name), args)) + } + + Expression::MemberAccess { + object, property, .. + } => { + let obj = self.transpile_expression(object)?; + // Handle built-in properties + let prop = match property.as_str() { + "length" => "length", + _ => property, + }; + Ok(format!("{}.{}", obj, self.sanitize_name(prop))) + } + + Expression::MethodCall { + object, + method, + arguments, + .. + } => { + let obj = self.transpile_expression(object)?; + let args = arguments + .iter() + .map(|a| self.transpile_argument(a)) + .collect::, _>>()? + .join(", "); + Ok(format!("{}.{}({})", obj, self.sanitize_name(method), args)) + } + + Expression::PropertyAccess { + object, property, .. + } => { + let obj = self.transpile_expression(object)?; + Ok(format!("{}.{}", obj, self.sanitize_name(property))) + } + + Expression::IndexAccess { + collection, index, .. + } => { + let coll = self.transpile_expression(collection)?; + let idx = self.transpile_expression(index)?; + Ok(format!("{}[{}]", coll, idx)) + } + + Expression::Concatenation { left, right, .. } => { + let l = self.transpile_expression(left)?; + let r = self.transpile_expression(right)?; + Ok(format!("(String({}) + String({}))", l, r)) + } + + Expression::PatternMatch { text, pattern, .. } => { + let t = self.transpile_expression(text)?; + let p = self.transpile_expression(pattern)?; + Ok(format!("{}.match({})", p, t)) + } + + Expression::PatternFind { text, pattern, .. } => { + let t = self.transpile_expression(text)?; + let p = self.transpile_expression(pattern)?; + Ok(format!("{}.find({})", p, t)) + } + + Expression::PatternReplace { + text, + pattern, + replacement, + .. + } => { + let t = self.transpile_expression(text)?; + let p = self.transpile_expression(pattern)?; + let r = self.transpile_expression(replacement)?; + Ok(format!("{}.replace({}, {})", p, t, r)) + } + + Expression::PatternSplit { text, pattern, .. } => { + let t = self.transpile_expression(text)?; + let p = self.transpile_expression(pattern)?; + Ok(format!("{}.split({})", p, t)) + } + + Expression::StringSplit { + text, delimiter, .. + } => { + let t = self.transpile_expression(text)?; + let d = self.transpile_expression(delimiter)?; + Ok(format!("({}).split({})", t, d)) + } + + Expression::AwaitExpression { expression, .. } => { + let expr = self.transpile_expression(expression)?; + Ok(format!("(await {})", expr)) + } + + Expression::StaticMemberAccess { + container, member, .. + } => Ok(format!( + "{}.{}", + self.sanitize_name(container), + self.sanitize_name(member) + )), + + Expression::HeaderAccess { + header_name, + request, + .. + } => { + let req = self.transpile_expression(request)?; + Ok(format!("{}.headers['{}']", req, header_name.to_lowercase())) + } + + Expression::CurrentTimeMilliseconds { .. } => Ok("Date.now()".to_string()), + + Expression::CurrentTimeFormatted { format, .. } => { + Ok(format!("WFL.time.format(new Date(), '{}')", format)) + } + + Expression::FileExists { path, .. } => { + let p = self.transpile_expression(path)?; + Ok(format!("WFL.file.exists({})", p)) + } + + Expression::DirectoryExists { path, .. } => { + let p = self.transpile_expression(path)?; + Ok(format!("WFL.directory.exists({})", p)) + } + + Expression::ListFiles { path, .. } => { + let p = self.transpile_expression(path)?; + Ok(format!("WFL.directory.list({})", p)) + } + + Expression::ListFilesRecursive { + path, extensions, .. + } => { + let p = self.transpile_expression(path)?; + let ext = extensions + .as_ref() + .map(|exts| { + exts.iter() + .map(|e| self.transpile_expression(e)) + .collect::, _>>() + }) + .transpose()? + .map(|v| format!("[{}]", v.join(", "))) + .unwrap_or_else(|| "[]".to_string()); + Ok(format!("WFL.directory.listRecursive({}, {})", p, ext)) + } + + Expression::ListFilesFiltered { + path, extensions, .. + } => { + let p = self.transpile_expression(path)?; + let ext = extensions + .iter() + .map(|e| self.transpile_expression(e)) + .collect::, _>>()? + .join(", "); + Ok(format!("WFL.directory.listRecursive({}, [{}])", p, ext)) + } + + Expression::ReadContent { file_handle, .. } => { + let fh = self.transpile_expression(file_handle)?; + Ok(format!("WFL.file.read({}.path)", fh)) + } + + Expression::ProcessRunning { process_id, .. } => { + let pid = self.transpile_expression(process_id)?; + Ok(format!("WFL.process.isRunning({})", pid)) + } + } + } + + /// Transpile a literal to JavaScript + fn transpile_literal(&self, lit: &Literal) -> Result { + match lit { + Literal::String(s) => Ok(format!("\"{}\"", self.escape_string(s))), + Literal::Integer(i) => Ok(i.to_string()), + Literal::Float(f) => Ok(f.to_string()), + Literal::Boolean(b) => Ok(if *b { "true" } else { "false" }.to_string()), + Literal::Nothing => Ok("null".to_string()), + Literal::Pattern(p) => Ok(format!("new WFL.Pattern({})", self.pattern_to_regex(p))), + Literal::List(items) => { + let elements = items + .iter() + .map(|e| self.clone().transpile_expression(e)) + .collect::, _>>()? + .join(", "); + Ok(format!("[{}]", elements)) + } + } + } + + /// Transpile an operator to JavaScript + fn transpile_operator(&self, op: &Operator) -> &'static str { + match op { + Operator::Plus => "+", + Operator::Minus => "-", + Operator::Multiply => "*", + Operator::Divide => "/", + Operator::Modulo => "%", + Operator::Equals => "===", + Operator::NotEquals => "!==", + Operator::GreaterThan => ">", + Operator::LessThan => "<", + Operator::GreaterThanOrEqual => ">=", + Operator::LessThanOrEqual => "<=", + Operator::And => "&&", + Operator::Or => "||", + Operator::Contains => "includes", // Handled specially in transpile_expression + } + } + + /// Transpile a parameter to JavaScript + fn transpile_parameter(&self, param: &Parameter) -> Result { + let name = self.sanitize_name(¶m.name); + if let Some(default) = ¶m.default_value { + // Clone self to transpile the expression (since we need mutable access) + let mut cloned = self.clone(); + let default_val = cloned + .transpile_expression(default) + .map_err(|e| TranspileError { + message: format!( + "Failed to transpile default value for parameter '{}': {}", + name, e + ), + line: e.line, + column: e.column, + })?; + Ok(format!("{} = {}", name, default_val)) + } else { + Ok(name) + } + } + + /// Transpile an argument to JavaScript + fn transpile_argument(&mut self, arg: &Argument) -> Result { + self.transpile_expression(&arg.value) + } + + /// Transpile a pattern expression to a JavaScript regex string + fn transpile_pattern_expression( + &self, + pattern: &PatternExpression, + ) -> Result { + let regex = self.pattern_expr_to_regex(pattern)?; + Ok(format!("/{}/", regex)) + } + + /// Convert a WFL pattern expression to a regex string + #[allow(clippy::only_used_in_recursion)] + fn pattern_expr_to_regex(&self, pattern: &PatternExpression) -> Result { + match pattern { + PatternExpression::Literal(s) => Ok(regex_escape(s)), + PatternExpression::CharacterClass(class) => Ok(match class { + CharClass::Digit => r"\d".to_string(), + CharClass::Letter => r"[a-zA-Z]".to_string(), + CharClass::Whitespace => r"\s".to_string(), + CharClass::Any => ".".to_string(), + CharClass::UnicodeCategory(cat) => format!(r"\p{{{}}}", cat), + CharClass::UnicodeScript(script) => format!(r"\p{{Script={}}}", script), + CharClass::UnicodeProperty(prop) => format!(r"\p{{{}}}", prop), + }), + PatternExpression::Quantified { + pattern, + quantifier, + } => { + let inner = self.pattern_expr_to_regex(pattern)?; + let quant = match quantifier { + Quantifier::Optional => "?", + Quantifier::ZeroOrMore => "*", + Quantifier::OneOrMore => "+", + Quantifier::Exactly(n) => return Ok(format!("(?:{}){{{}}}", inner, n)), + Quantifier::Between(min, max) => { + return Ok(format!("(?:{}){{{},{}}}", inner, min, max)); + } + Quantifier::AtLeast(n) => return Ok(format!("(?:{}){{{},}}", inner, n)), + Quantifier::AtMost(n) => return Ok(format!("(?:{}){{0,{}}}", inner, n)), + }; + Ok(format!("(?:{}){}", inner, quant)) + } + PatternExpression::Sequence(patterns) => { + let parts: Result, _> = patterns + .iter() + .map(|p| self.pattern_expr_to_regex(p)) + .collect(); + Ok(parts?.join("")) + } + PatternExpression::Alternative(patterns) => { + let parts: Result, _> = patterns + .iter() + .map(|p| self.pattern_expr_to_regex(p)) + .collect(); + Ok(format!("(?:{})", parts?.join("|"))) + } + PatternExpression::Capture { name, pattern } => { + let inner = self.pattern_expr_to_regex(pattern)?; + Ok(format!("(?<{}>{})", name, inner)) + } + PatternExpression::Backreference(name) => Ok(format!(r"\k<{}>", name)), + PatternExpression::Anchor(anchor) => Ok(match anchor { + Anchor::StartOfText => "^".to_string(), + Anchor::EndOfText => "$".to_string(), + }), + PatternExpression::Lookahead(pattern) => { + let inner = self.pattern_expr_to_regex(pattern)?; + Ok(format!("(?={})", inner)) + } + PatternExpression::NegativeLookahead(pattern) => { + let inner = self.pattern_expr_to_regex(pattern)?; + Ok(format!("(?!{})", inner)) + } + PatternExpression::Lookbehind(pattern) => { + let inner = self.pattern_expr_to_regex(pattern)?; + Ok(format!("(?<={})", inner)) + } + PatternExpression::NegativeLookbehind(pattern) => { + let inner = self.pattern_expr_to_regex(pattern)?; + Ok(format!("(? { + // This would need runtime support to work properly + Ok(format!("(?:${{{}}})", name)) + } + } + } + + /// Convert a simple pattern string to a regex + fn pattern_to_regex(&self, pattern: &str) -> String { + // For simple patterns, just escape regex special characters + format!("\"{}\"", regex_escape(pattern)) + } + + /// Check if a list of statements contains any async operations + fn contains_async(&self, stmts: &[Statement]) -> bool { + for stmt in stmts { + if self.stmt_is_async(stmt) { + return true; + } + } + false + } + + /// Check if a statement is or contains async operations + fn stmt_is_async(&self, stmt: &Statement) -> bool { + match stmt { + Statement::WaitForStatement { .. } + | Statement::WaitForDurationStatement { .. } + | Statement::HttpGetStatement { .. } + | Statement::HttpPostStatement { .. } + | Statement::WaitForProcessStatement { .. } + | Statement::WaitForRequestStatement { .. } => true, + + Statement::IfStatement { + then_block, + else_block, + .. + } => { + self.contains_async(then_block) + || else_block + .as_ref() + .map(|b| self.contains_async(b)) + .unwrap_or(false) + } + + Statement::ForEachLoop { body, .. } + | Statement::CountLoop { body, .. } + | Statement::WhileLoop { body, .. } + | Statement::RepeatWhileLoop { body, .. } + | Statement::RepeatUntilLoop { body, .. } + | Statement::ForeverLoop { body, .. } + | Statement::MainLoop { body, .. } => self.contains_async(body), + + Statement::TryStatement { + body, + when_clauses, + otherwise_block, + .. + } => { + self.contains_async(body) + || when_clauses.iter().any(|c| self.contains_async(&c.body)) + || otherwise_block + .as_ref() + .map(|b| self.contains_async(b)) + .unwrap_or(false) + } + + Statement::ExpressionStatement { expression, .. } => self.expr_is_async(expression), + + _ => false, + } + } + + /// Check if an expression contains async operations + fn expr_is_async(&self, expr: &Expression) -> bool { + matches!(expr, Expression::AwaitExpression { .. }) + } + + /// Sanitize a WFL identifier to be a valid JavaScript identifier + fn sanitize_name(&self, name: &str) -> String { + // Handle reserved JavaScript keywords + let reserved = [ + "break", + "case", + "catch", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "finally", + "for", + "function", + "if", + "in", + "instanceof", + "new", + "return", + "switch", + "this", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with", + "class", + "const", + "enum", + "export", + "extends", + "import", + "super", + "implements", + "interface", + "let", + "package", + "private", + "protected", + "public", + "static", + "yield", + "await", + "async", + ]; + + let mut result = name.to_string(); + + // Replace spaces and dashes with underscores + result = result.replace([' ', '-'], "_"); + + // If starts with a digit, prefix with underscore + if result + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + result = format!("_{}", result); + } + + // If it's a reserved word, prefix with underscore + if reserved.contains(&result.as_str()) { + result = format!("_{}", result); + } + + result + } + + /// Escape a string for JavaScript + fn escape_string(&self, s: &str) -> String { + s.replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t") + } + + /// Escape a key for JavaScript object literal + fn escape_key(&self, key: &str) -> String { + // Check if key is a valid identifier + let is_valid_identifier = key + .chars() + .next() + .map(|c| c.is_alphabetic() || c == '_') + .unwrap_or(false) + && key.chars().all(|c| c.is_alphanumeric() || c == '_'); + + if is_valid_identifier { + key.to_string() + } else { + format!("\"{}\"", self.escape_string(key)) + } + } +} + +impl Clone for JavaScriptTranspiler { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + indent_level: self.indent_level, + warnings: Vec::new(), // Don't clone warnings + in_async: self.in_async, + } + } +} + +/// Escape special regex characters in a string +fn regex_escape(s: &str) -> String { + let special_chars = [ + '.', '*', '+', '?', '^', '$', '{', '}', '[', ']', '(', ')', '|', '\\', + ]; + let mut result = String::with_capacity(s.len() * 2); + for c in s.chars() { + if special_chars.contains(&c) { + result.push('\\'); + } + result.push(c); + } + result +} diff --git a/src/transpiler/mod.rs b/src/transpiler/mod.rs new file mode 100644 index 00000000..fee7d3f0 --- /dev/null +++ b/src/transpiler/mod.rs @@ -0,0 +1,103 @@ +//! WFL to JavaScript Transpiler +//! +//! This module provides functionality to transpile WFL (Web First Language) code +//! into JavaScript, allowing WFL programs to run in browsers or Node.js environments. + +mod javascript; +mod runtime; + +pub use javascript::JavaScriptTranspiler; + +use crate::parser::ast::Program; + +/// Configuration options for the transpiler +#[derive(Debug, Clone)] +pub struct TranspilerConfig { + /// Whether to include the runtime library in the output + pub include_runtime: bool, + /// Whether to generate source maps (future feature) + pub source_maps: bool, + /// Target environment (browser or node) + pub target: TranspilerTarget, + /// Whether to minify the output (future feature) + pub minify: bool, + /// Indentation string (e.g., " " or "\t") + pub indent: String, + /// Whether to generate ES modules (export/import) or IIFE + pub es_modules: bool, +} + +impl Default for TranspilerConfig { + fn default() -> Self { + Self { + include_runtime: true, + source_maps: false, + target: TranspilerTarget::Node, + minify: false, + indent: " ".to_string(), + es_modules: false, + } + } +} + +/// Target environment for the transpiled code +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TranspilerTarget { + /// Node.js environment + Node, + /// Browser environment + Browser, + /// Universal (works in both) + Universal, +} + +/// Result of a transpilation operation +#[derive(Debug)] +pub struct TranspileResult { + /// The generated JavaScript code + pub code: String, + /// Any warnings generated during transpilation + pub warnings: Vec, +} + +/// A warning generated during transpilation +#[derive(Debug)] +pub struct TranspileWarning { + pub message: String, + pub line: usize, + pub column: usize, +} + +/// Error that can occur during transpilation +#[derive(Debug)] +pub struct TranspileError { + pub message: String, + pub line: usize, + pub column: usize, +} + +impl std::fmt::Display for TranspileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Transpile error at line {}, column {}: {}", + self.line, self.column, self.message + ) + } +} + +impl std::error::Error for TranspileError {} + +/// Main entry point for transpiling WFL to JavaScript +pub fn transpile( + program: &Program, + config: &TranspilerConfig, +) -> Result { + let transpiler = JavaScriptTranspiler::new(config.clone()); + transpiler.transpile(program) +} + +/// Convenience function to transpile with default configuration +pub fn transpile_default(program: &Program) -> Result { + transpile(program, &TranspilerConfig::default()) +} diff --git a/src/transpiler/runtime.rs b/src/transpiler/runtime.rs new file mode 100644 index 00000000..ea52111e --- /dev/null +++ b/src/transpiler/runtime.rs @@ -0,0 +1,709 @@ +//! WFL JavaScript Runtime Library +//! +//! This module contains the JavaScript runtime library that provides +//! WFL-specific functionality in the transpiled output. + +/// The WFL JavaScript runtime library for Node.js +pub const RUNTIME_NODE: &str = r#"// WFL Runtime Library for Node.js +// This runtime provides WFL-specific functionality + +const fs = require('fs'); +const path = require('path'); +const { spawn, spawnSync } = require('child_process'); + +// WFL Runtime namespace +const WFL = { + // Type checking utilities + typeof: (value) => { + if (value === null || value === undefined) return 'nothing'; + if (Array.isArray(value)) return 'list'; + if (value instanceof Date) return 'date'; + if (value instanceof WFL.Pattern) return 'pattern'; + if (value instanceof WFL.Container) return 'container'; + if (typeof value === 'object' && value._wfl_container) return 'container_instance'; + const t = typeof value; + if (t === 'number') return 'number'; + if (t === 'string') return 'text'; + if (t === 'boolean') return 'boolean'; + if (t === 'function') return 'action'; + return 'object'; + }, + + // Convert WFL value to string for display + stringify: (value) => { + if (value === null || value === undefined) return 'nothing'; + if (Array.isArray(value)) { + return '[' + value.map(v => WFL.stringify(v)).join(', ') + ']'; + } + if (typeof value === 'object') { + if (value._wfl_container) { + return `<${value._wfl_type} instance>`; + } + return JSON.stringify(value); + } + return String(value); + }, + + // Display function (print to console) + display: (...args) => { + console.log(args.map(WFL.stringify).join(' ')); + }, + + // String operations + text: { + length: (s) => String(s).length, + uppercase: (s) => String(s).toUpperCase(), + lowercase: (s) => String(s).toLowerCase(), + trim: (s) => String(s).trim(), + substring: (s, start, end) => String(s).substring(start, end), + indexOf: (s, search) => String(s).indexOf(search), + replace: (s, search, replacement) => String(s).replace(search, replacement), + replaceAll: (s, search, replacement) => String(s).split(search).join(replacement), + split: (s, delimiter) => String(s).split(delimiter), + startsWith: (s, prefix) => String(s).startsWith(prefix), + endsWith: (s, suffix) => String(s).endsWith(suffix), + contains: (s, search) => String(s).includes(search), + charAt: (s, index) => String(s).charAt(index), + concat: (...args) => args.map(String).join(''), + }, + + // Math operations + math: { + abs: Math.abs, + round: Math.round, + floor: Math.floor, + ceil: Math.ceil, + sqrt: Math.sqrt, + pow: Math.pow, + min: Math.min, + max: Math.max, + random: Math.random, + randomInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + log: Math.log, + exp: Math.exp, + PI: Math.PI, + E: Math.E, + }, + + // List operations + list: { + create: (...items) => [...items], + push: (list, item) => { list.push(item); return list; }, + pop: (list) => list.pop(), + shift: (list) => list.shift(), + unshift: (list, item) => { list.unshift(item); return list; }, + length: (list) => list.length, + get: (list, index) => list[index], + set: (list, index, value) => { list[index] = value; return list; }, + contains: (list, item) => list.includes(item), + indexOf: (list, item) => list.indexOf(item), + remove: (list, item) => { + const idx = list.indexOf(item); + if (idx > -1) list.splice(idx, 1); + return list; + }, + removeAt: (list, index) => { list.splice(index, 1); return list; }, + clear: (list) => { list.length = 0; return list; }, + slice: (list, start, end) => list.slice(start, end), + concat: (...lists) => [].concat(...lists), + reverse: (list) => [...list].reverse(), + sort: (list, compareFn) => [...list].sort(compareFn), + map: (list, fn) => list.map(fn), + filter: (list, fn) => list.filter(fn), + reduce: (list, fn, initial) => list.reduce(fn, initial), + forEach: (list, fn) => list.forEach(fn), + find: (list, fn) => list.find(fn), + every: (list, fn) => list.every(fn), + some: (list, fn) => list.some(fn), + join: (list, separator) => list.join(separator || ''), + }, + + // Map/Object operations + map: { + create: (entries) => { + const obj = {}; + if (entries) { + for (const [k, v] of Object.entries(entries)) { + obj[k] = v; + } + } + return obj; + }, + get: (map, key) => map[key], + set: (map, key, value) => { map[key] = value; return map; }, + has: (map, key) => key in map, + delete: (map, key) => { delete map[key]; return map; }, + keys: (map) => Object.keys(map), + values: (map) => Object.values(map), + entries: (map) => Object.entries(map), + size: (map) => Object.keys(map).length, + }, + + // File operations (Node.js) + file: { + _validatePath: (filepath) => { + const resolved = path.resolve(filepath); + // Define allowed base directory (current working directory by default) + const baseDir = path.resolve('.'); + // Use path.relative to check if the resolved path is within the base directory + const relative = path.relative(baseDir, resolved); + // Reject if the relative path starts with '..' (goes outside baseDir) + // or if it's an absolute path (shouldn't happen with path.relative from same root) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Path traversal detected: ${filepath} resolves outside of ${baseDir}`); + } + return resolved; + }, + read: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.readFileSync(validPath, 'utf8'); + }, + write: (filepath, content) => { + const validPath = WFL.file._validatePath(filepath); + return fs.writeFileSync(validPath, content, 'utf8'); + }, + append: (filepath, content) => { + const validPath = WFL.file._validatePath(filepath); + return fs.appendFileSync(validPath, content, 'utf8'); + }, + exists: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.existsSync(validPath); + }, + delete: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.unlinkSync(validPath); + }, + copy: (src, dest) => { + const validSrc = WFL.file._validatePath(src); + const validDest = WFL.file._validatePath(dest); + return fs.copyFileSync(validSrc, validDest); + }, + move: (src, dest) => { + const validSrc = WFL.file._validatePath(src); + const validDest = WFL.file._validatePath(dest); + return fs.renameSync(validSrc, validDest); + }, + size: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.statSync(validPath).size; + }, + isFile: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.existsSync(validPath) && fs.statSync(validPath).isFile(); + }, + isDirectory: (filepath) => { + const validPath = WFL.file._validatePath(filepath); + return fs.existsSync(validPath) && fs.statSync(validPath).isDirectory(); + }, + }, + + // Directory operations (Node.js) + directory: { + create: (dirpath) => { + const validPath = WFL.file._validatePath(dirpath); + return fs.mkdirSync(validPath, { recursive: true }); + }, + delete: (dirpath) => { + const validPath = WFL.file._validatePath(dirpath); + return fs.rmSync(validPath, { recursive: true, force: true }); + }, + list: (dirpath) => { + const validPath = WFL.file._validatePath(dirpath); + return fs.readdirSync(validPath); + }, + listRecursive: (dirpath, extensions) => { + const validPath = WFL.file._validatePath(dirpath); + const results = []; + const walk = (dir) => { + const files = fs.readdirSync(dir); + for (const file of files) { + const filepath = path.join(dir, file); + const stat = fs.statSync(filepath); + if (stat.isDirectory()) { + walk(filepath); + } else { + if (!extensions || extensions.length === 0 || + extensions.some(ext => filepath.endsWith(ext))) { + results.push(filepath); + } + } + } + }; + walk(validPath); + return results; + }, + exists: (dirpath) => { + const validPath = WFL.file._validatePath(dirpath); + return fs.existsSync(validPath) && fs.statSync(validPath).isDirectory(); + }, + }, + + // Date/Time operations + time: { + now: () => Date.now(), + today: () => { + const d = new Date(); + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + }, + format: (date, format) => { + const d = date instanceof Date ? date : new Date(date); + const pad = (n) => String(n).padStart(2, '0'); + return format + .replace('YYYY', d.getFullYear()) + .replace('MM', pad(d.getMonth() + 1)) + .replace('DD', pad(d.getDate())) + .replace('HH', pad(d.getHours())) + .replace('mm', pad(d.getMinutes())) + .replace('ss', pad(d.getSeconds())); + }, + parse: (str) => new Date(str), + year: (date) => (date instanceof Date ? date : new Date(date)).getFullYear(), + month: (date) => (date instanceof Date ? date : new Date(date)).getMonth() + 1, + day: (date) => (date instanceof Date ? date : new Date(date)).getDate(), + hours: (date) => (date instanceof Date ? date : new Date(date)).getHours(), + minutes: (date) => (date instanceof Date ? date : new Date(date)).getMinutes(), + seconds: (date) => (date instanceof Date ? date : new Date(date)).getSeconds(), + milliseconds: (date) => (date instanceof Date ? date : new Date(date)).getMilliseconds(), + }, + + // Process operations (Node.js) + process: { + execute: (command, args) => { + const result = spawnSync(command, args || [], { encoding: 'utf8' }); + if (result.error) throw result.error; + if (result.status !== 0) { + const error = new Error(`Command failed with exit code ${result.status}`); + error.code = result.status; + error.stderr = result.stderr; + throw error; + } + return result.stdout; + }, + spawn: (command, args) => { + const proc = spawn(command, args || [], { stdio: 'pipe' }); + return { + _process: proc, + pid: proc.pid, + stdout: '', + stderr: '', + exitCode: null, + running: true, + }; + }, + kill: (proc) => { + if (proc._process) proc._process.kill(); + proc.running = false; + }, + isRunning: (proc) => proc.running, + }, + + // Pattern matching + Pattern: class Pattern { + constructor(regex) { + this.regex = regex instanceof RegExp ? regex : new RegExp(regex); + } + match(text) { + return this.regex.test(text); + } + find(text) { + const match = text.match(this.regex); + return match ? match[0] : null; + } + findAll(text) { + const globalRegex = new RegExp(this.regex.source, 'g'); + return text.match(globalRegex) || []; + } + replace(text, replacement) { + return text.replace(this.regex, replacement); + } + replaceAll(text, replacement) { + const globalRegex = new RegExp(this.regex.source, 'g'); + return text.replace(globalRegex, replacement); + } + split(text) { + return text.split(this.regex); + } + }, + + // Container (class) base + Container: class Container { + constructor() { + this._wfl_container = true; + this._wfl_type = this.constructor.name; + } + }, + + // HTTP operations (Node.js) + http: { + get: async (url, timeout = 10000) => { + const https = url.startsWith('https') ? require('https') : require('http'); + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Request timeout after ${timeout}ms`)); + }, timeout); + + const req = https.get(url, (res) => { + clearTimeout(timeoutId); + + // Check HTTP status code + if (res.statusCode < 200 || res.statusCode >= 400) { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`)); + return; + } + + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + res.on('error', reject); + }); + + req.on('error', (err) => { + clearTimeout(timeoutId); + reject(err); + }); + req.setTimeout(timeout, () => { + req.destroy(); + reject(new Error(`Request timeout after ${timeout}ms`)); + }); + }); + }, + post: async (url, data, timeout = 10000) => { + const https = url.startsWith('https') ? require('https') : require('http'); + const urlObj = new URL(url); + const postData = typeof data === 'string' ? data : JSON.stringify(data); + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new Error(`Request timeout after ${timeout}ms`)); + }, timeout); + + const req = https.request({ + hostname: urlObj.hostname, + port: urlObj.port, + path: urlObj.pathname + urlObj.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }, (res) => { + clearTimeout(timeoutId); + + // Check HTTP status code + if (res.statusCode < 200 || res.statusCode >= 400) { + let errorBody = ''; + res.on('data', chunk => errorBody += chunk); + res.on('end', () => { + reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage} - ${errorBody}`)); + }); + return; + } + + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => resolve(body)); + res.on('error', reject); + }); + + req.on('error', (err) => { + clearTimeout(timeoutId); + reject(err); + }); + req.setTimeout(timeout, () => { + req.destroy(); + reject(new Error(`Request timeout after ${timeout}ms`)); + }); + + req.write(postData); + req.end(); + }); + }, + }, + + // Utility functions + sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)), + exit: (code) => process.exit(code || 0), +}; + +// Make WFL available globally in Node.js +if (typeof module !== 'undefined' && module.exports) { + module.exports = WFL; +} +if (typeof global !== 'undefined') { + global.WFL = WFL; +} +"#; + +/// The WFL JavaScript runtime library for browsers +pub const RUNTIME_BROWSER: &str = r#"// WFL Runtime Library for Browsers +// This runtime provides WFL-specific functionality + +const WFL = { + // Type checking utilities + typeof: (value) => { + if (value === null || value === undefined) return 'nothing'; + if (Array.isArray(value)) return 'list'; + if (value instanceof Date) return 'date'; + if (value instanceof WFL.Pattern) return 'pattern'; + if (value instanceof WFL.Container) return 'container'; + if (typeof value === 'object' && value._wfl_container) return 'container_instance'; + const t = typeof value; + if (t === 'number') return 'number'; + if (t === 'string') return 'text'; + if (t === 'boolean') return 'boolean'; + if (t === 'function') return 'action'; + return 'object'; + }, + + // Convert WFL value to string for display + stringify: (value) => { + if (value === null || value === undefined) return 'nothing'; + if (Array.isArray(value)) { + return '[' + value.map(v => WFL.stringify(v)).join(', ') + ']'; + } + if (typeof value === 'object') { + if (value._wfl_container) { + return `<${value._wfl_type} instance>`; + } + return JSON.stringify(value); + } + return String(value); + }, + + // Display function (print to console) + display: (...args) => { + console.log(args.map(WFL.stringify).join(' ')); + }, + + // String operations + text: { + length: (s) => String(s).length, + uppercase: (s) => String(s).toUpperCase(), + lowercase: (s) => String(s).toLowerCase(), + trim: (s) => String(s).trim(), + substring: (s, start, end) => String(s).substring(start, end), + indexOf: (s, search) => String(s).indexOf(search), + replace: (s, search, replacement) => String(s).replace(search, replacement), + replaceAll: (s, search, replacement) => String(s).split(search).join(replacement), + split: (s, delimiter) => String(s).split(delimiter), + startsWith: (s, prefix) => String(s).startsWith(prefix), + endsWith: (s, suffix) => String(s).endsWith(suffix), + contains: (s, search) => String(s).includes(search), + charAt: (s, index) => String(s).charAt(index), + concat: (...args) => args.map(String).join(''), + }, + + // Math operations + math: { + abs: Math.abs, + round: Math.round, + floor: Math.floor, + ceil: Math.ceil, + sqrt: Math.sqrt, + pow: Math.pow, + min: Math.min, + max: Math.max, + random: Math.random, + randomInt: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + log: Math.log, + exp: Math.exp, + PI: Math.PI, + E: Math.E, + }, + + // List operations + list: { + create: (...items) => [...items], + push: (list, item) => { list.push(item); return list; }, + pop: (list) => list.pop(), + shift: (list) => list.shift(), + unshift: (list, item) => { list.unshift(item); return list; }, + length: (list) => list.length, + get: (list, index) => list[index], + set: (list, index, value) => { list[index] = value; return list; }, + contains: (list, item) => list.includes(item), + indexOf: (list, item) => list.indexOf(item), + remove: (list, item) => { + const idx = list.indexOf(item); + if (idx > -1) list.splice(idx, 1); + return list; + }, + removeAt: (list, index) => { list.splice(index, 1); return list; }, + clear: (list) => { list.length = 0; return list; }, + slice: (list, start, end) => list.slice(start, end), + concat: (...lists) => [].concat(...lists), + reverse: (list) => [...list].reverse(), + sort: (list, compareFn) => [...list].sort(compareFn), + map: (list, fn) => list.map(fn), + filter: (list, fn) => list.filter(fn), + reduce: (list, fn, initial) => list.reduce(fn, initial), + forEach: (list, fn) => list.forEach(fn), + find: (list, fn) => list.find(fn), + every: (list, fn) => list.every(fn), + some: (list, fn) => list.some(fn), + join: (list, separator) => list.join(separator || ''), + }, + + // Map/Object operations + map: { + create: (entries) => { + const obj = {}; + if (entries) { + for (const [k, v] of Object.entries(entries)) { + obj[k] = v; + } + } + return obj; + }, + get: (map, key) => map[key], + set: (map, key, value) => { map[key] = value; return map; }, + has: (map, key) => key in map, + delete: (map, key) => { delete map[key]; return map; }, + keys: (map) => Object.keys(map), + values: (map) => Object.values(map), + entries: (map) => Object.entries(map), + size: (map) => Object.keys(map).length, + }, + + // Date/Time operations + time: { + now: () => Date.now(), + today: () => { + const d = new Date(); + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + }, + format: (date, format) => { + const d = date instanceof Date ? date : new Date(date); + const pad = (n) => String(n).padStart(2, '0'); + return format + .replace('YYYY', d.getFullYear()) + .replace('MM', pad(d.getMonth() + 1)) + .replace('DD', pad(d.getDate())) + .replace('HH', pad(d.getHours())) + .replace('mm', pad(d.getMinutes())) + .replace('ss', pad(d.getSeconds())); + }, + parse: (str) => new Date(str), + year: (date) => (date instanceof Date ? date : new Date(date)).getFullYear(), + month: (date) => (date instanceof Date ? date : new Date(date)).getMonth() + 1, + day: (date) => (date instanceof Date ? date : new Date(date)).getDate(), + hours: (date) => (date instanceof Date ? date : new Date(date)).getHours(), + minutes: (date) => (date instanceof Date ? date : new Date(date)).getMinutes(), + seconds: (date) => (date instanceof Date ? date : new Date(date)).getSeconds(), + milliseconds: (date) => (date instanceof Date ? date : new Date(date)).getMilliseconds(), + }, + + // Pattern matching + Pattern: class Pattern { + constructor(regex) { + this.regex = regex instanceof RegExp ? regex : new RegExp(regex); + } + match(text) { + return this.regex.test(text); + } + find(text) { + const match = text.match(this.regex); + return match ? match[0] : null; + } + findAll(text) { + const globalRegex = new RegExp(this.regex.source, 'g'); + return text.match(globalRegex) || []; + } + replace(text, replacement) { + return text.replace(this.regex, replacement); + } + replaceAll(text, replacement) { + const globalRegex = new RegExp(this.regex.source, 'g'); + return text.replace(globalRegex, replacement); + } + split(text) { + return text.split(this.regex); + } + }, + + // Container (class) base + Container: class Container { + constructor() { + this._wfl_container = true; + this._wfl_type = this.constructor.name; + } + }, + + // HTTP operations (Browser - using fetch) + http: { + get: async (url, timeout = 10000) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + signal: controller.signal + }); + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`); + } + + return response.text(); + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${timeout}ms`); + } + throw error; + } + }, + post: async (url, data, timeout = 10000) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: typeof data === 'string' ? data : JSON.stringify(data), + signal: controller.signal + }); + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`); + } + + return response.text(); + } catch (error) { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${timeout}ms`); + } + throw error; + } + }, + }, + + // Utility functions + sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)), + exit: (code) => { throw new Error(`Program exited with code ${code || 0}`); }, +}; + +// Make WFL available globally +window.WFL = WFL; +"#; + +/// Returns the runtime library for the specified target +pub fn get_runtime(target: super::TranspilerTarget) -> &'static str { + match target { + super::TranspilerTarget::Node => RUNTIME_NODE, + super::TranspilerTarget::Browser => RUNTIME_BROWSER, + super::TranspilerTarget::Universal => RUNTIME_NODE, // Default to Node.js version + } +} diff --git a/tests/transpiler_test.rs b/tests/transpiler_test.rs new file mode 100644 index 00000000..9bb82617 --- /dev/null +++ b/tests/transpiler_test.rs @@ -0,0 +1,576 @@ +//! Tests for the WFL to JavaScript transpiler + +use wfl::lexer::lex_wfl_with_positions; +use wfl::parser::Parser; +use wfl::transpiler::{TranspilerConfig, TranspilerTarget, transpile}; + +/// Helper function to parse WFL source code and transpile to JavaScript +fn transpile_wfl(source: &str) -> Result { + let tokens = lex_wfl_with_positions(source); + let mut parser = Parser::new(&tokens); + let program = parser + .parse() + .map_err(|e| format!("Parse error: {:?}", e))?; + + let config = TranspilerConfig { + include_runtime: false, // Don't include runtime for tests (cleaner output) + source_maps: false, + target: TranspilerTarget::Node, + minify: false, + indent: " ".to_string(), + es_modules: false, + }; + + let result = transpile(&program, &config).map_err(|e| format!("Transpile error: {}", e))?; + Ok(result.code) +} + +/// Helper to check if output contains expected JavaScript +fn assert_contains(output: &str, expected: &str) { + assert!( + output.contains(expected), + "Expected output to contain:\n{}\n\nActual output:\n{}", + expected, + output + ); +} + +#[test] +fn test_variable_declaration() { + let source = r#"store name as "Alice""#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, r#"let name = "Alice";"#); +} + +#[test] +fn test_variable_with_spaces() { + let source = r#"store user name as "Bob""#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, r#"let user_name = "Bob";"#); +} + +#[test] +fn test_variable_assignment() { + let source = r#" +store x as 10 +change x to 20 +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let x = 10;"); + assert_contains(&js, "x = 20;"); +} + +#[test] +fn test_display_statement() { + let source = r#"display "Hello, World!""#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, r#"WFL.display("Hello, World!");"#); +} + +#[test] +fn test_if_statement() { + let source = r#" +store x as 10 +check if x is greater than 5: + display "big" +end check +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "if ((x > 5))"); + assert_contains(&js, r#"WFL.display("big");"#); +} + +#[test] +fn test_if_else_statement() { + let source = r#" +store x as 3 +check if x is greater than 5: + display "big" +otherwise: + display "small" +end check +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "if ((x > 5))"); + assert_contains(&js, "} else {"); + assert_contains(&js, r#"WFL.display("small");"#); +} + +#[test] +fn test_count_loop() { + let source = r#" +count from 1 to 5: + display count +end count +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "for (let count = 1; count <= 5; count += 1)"); +} + +#[test] +fn test_count_loop_with_step() { + let source = r#" +count from 0 to 10 by 2: + display count +end count +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "for (let count = 0; count <= 10; count += 2)"); +} + +#[test] +fn test_count_loop_downward() { + let source = r#" +count from 10 down to 1: + display count +end count +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "for (let count = 10; count >= 1; count -= 1)"); +} + +#[test] +fn test_for_each_loop() { + let source = r#" +create list items: + add "a" + add "b" +end list +for each item in items: + display item +end for +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "for (const item of items)"); +} + +#[test] +fn test_action_definition() { + let source = r#" +define action called greet: + display "Hello!" +end action +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "function greet()"); + assert_contains(&js, r#"WFL.display("Hello!");"#); +} + +#[test] +fn test_action_with_parameter() { + let source = r#" +define action called say_hello with parameter name: + display name +end action +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "function say_hello"); + assert_contains(&js, "WFL.display(name);"); +} + +#[test] +fn test_action_with_return() { + let source = r#" +define action called double with parameter x: + return x times 2 +end action +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "function double"); + assert_contains(&js, "return (x * 2);"); +} + +#[test] +fn test_binary_arithmetic_operations() { + let source = r#" +store a as 10 plus 5 +store b as 10 minus 5 +store c as 10 times 5 +store d as 10 divided by 5 +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = (10 + 5);"); + assert_contains(&js, "let b = (10 - 5);"); + assert_contains(&js, "let c = (10 * 5);"); + assert_contains(&js, "let d = (10 / 5);"); +} + +#[test] +fn test_comparison_operations() { + // Use variables on the left side of comparisons (WFL syntax) + let source = r#" +store x as 5 +store y as 10 +store a as x is equal to x +store b as x is greater than 3 +store c as y is less than 15 +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = (x === x);"); + assert_contains(&js, "let b = (x > 3);"); + assert_contains(&js, "let c = (y < 15);"); +} + +#[test] +fn test_logical_operations() { + let source = r#" +store a as yes and no +store b as yes or no +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = (true && false);"); + assert_contains(&js, "let b = (true || false);"); +} + +#[test] +fn test_unary_not() { + let source = r#"store a as not yes"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = (!true);"); +} + +#[test] +fn test_list_creation() { + let source = r#" +create list numbers: + add 1 + add 2 + add 3 +end list +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let numbers = [1, 2, 3];"); +} + +#[test] +fn test_list_push() { + let source = r#" +create list items: +end list +push with items and "new item" +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "items.push(\"new item\");"); +} + +#[test] +fn test_list_clear() { + let source = r#" +create list items: + add 1 +end list +clear items +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "items.length = 0;"); +} + +#[test] +fn test_string_concatenation() { + let source = r#"store greeting as "Hello, " with "World!""#; + let js = transpile_wfl(source).unwrap(); + assert_contains( + &js, + r#"let greeting = (String("Hello, ") + String("World!"));"#, + ); +} + +#[test] +fn test_main_action_call() { + let source = r#" +define action called main: + display "Hello from main!" +end action +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "function main()"); + assert_contains(&js, "main();"); // Entry point call +} + +#[test] +fn test_es_modules_option() { + let source = r#"display "test""#; + let tokens = lex_wfl_with_positions(source); + let mut parser = Parser::new(&tokens); + let program = parser.parse().unwrap(); + + let config = TranspilerConfig { + include_runtime: false, + source_maps: false, + target: TranspilerTarget::Node, + minify: false, + indent: " ".to_string(), + es_modules: true, // Enable ES modules + }; + + let result = transpile(&program, &config).unwrap(); + + // Should NOT have IIFE wrapper when using ES modules + assert!(!result.code.contains("(function()")); +} + +#[test] +fn test_iife_wrapper() { + let source = r#"display "test""#; + let tokens = lex_wfl_with_positions(source); + let mut parser = Parser::new(&tokens); + let program = parser.parse().unwrap(); + + let config = TranspilerConfig { + include_runtime: false, + source_maps: false, + target: TranspilerTarget::Node, + minify: false, + indent: " ".to_string(), + es_modules: false, // Disable ES modules (use IIFE) + }; + + let result = transpile(&program, &config).unwrap(); + + // Should have IIFE wrapper + assert_contains(&result.code, "(function()"); + assert_contains(&result.code, "'use strict';"); + assert_contains(&result.code, "})();"); +} + +#[test] +fn test_boolean_literals() { + let source = r#" +store a as yes +store b as no +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = true;"); + assert_contains(&js, "let b = false;"); +} + +#[test] +fn test_nothing_literal() { + let source = r#"store a as nothing"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = null;"); +} + +#[test] +fn test_repeat_while_loop() { + let source = r#" +store x as 0 +repeat while x is less than 5: + change x to x plus 1 +end repeat +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "do {"); + assert_contains(&js, "} while ((x < 5));"); +} + +#[test] +fn test_reserved_word_sanitization() { + // Test that reserved words are properly sanitized + // Using 'function' as a variable name (reserved in JS) + let source = r#"store my_function as "test""#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let my_function ="); +} + +#[test] +fn test_try_catch_basic() { + let source = r#" +try: + display "trying" +catch: + display "caught" +end try +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "try {"); + assert_contains(&js, "} catch (_wfl_error) {"); +} + +#[test] +fn test_nested_if() { + let source = r#" +store x as 10 +check if x is greater than 5: + check if x is less than 15: + display "in range" + end check +end check +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "if ((x > 5))"); + assert_contains(&js, "if ((x < 15))"); + assert_contains(&js, r#"WFL.display("in range");"#); +} + +#[test] +fn test_multiple_statements() { + let source = r#" +store a as 1 +store b as 2 +store c as a plus b +display c +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let a = 1;"); + assert_contains(&js, "let b = 2;"); + assert_contains(&js, "let c = (a + b);"); + assert_contains(&js, "WFL.display(c);"); +} + +#[test] +fn test_number_literals() { + let source = r#" +store integer as 42 +store float_val as 3.14 +store negative as 0 minus 10 +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let integer = 42;"); + assert_contains(&js, "let float_val = 3.14;"); + assert_contains(&js, "let negative = (0 - 10);"); +} + +#[test] +fn test_display_concatenation() { + let source = r#" +store name as "Alice" +display "Hello, " with name +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, r#"WFL.display((String("Hello, ") + String(name)));"#); +} + +#[test] +fn test_complex_expression() { + let source = r#"store result as 2 plus 3 times 4"#; + let js = transpile_wfl(source).unwrap(); + // Should handle operator precedence + assert!(js.contains("let result =")); +} + +#[test] +fn test_empty_list() { + let source = r#" +create list empty_items: +end list +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let empty_items = [];"); +} + +#[test] +fn test_list_with_mixed_types() { + let source = r#" +create list mixed: + add 1 + add "text" + add yes +end list +"#; + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "let mixed = [1, \"text\", true];"); +} + +#[test] +fn test_action_hoisting() { + // Actions should be hoisted (generated before other code) + let source = r#" +display "before" +define action called my_action: + display "my_action" +end action +display "after" +"#; + let js = transpile_wfl(source).unwrap(); + // The function should appear before the display calls + let func_pos = js.find("function my_action()").unwrap(); + let display_before = js.find(r#"WFL.display("before")"#).unwrap(); + assert!( + func_pos < display_before, + "Function should be hoisted before other statements" + ); +} + +#[test] +fn test_runtime_inclusion() { + let source = r#" + store x as 42 + display x + "#; + + // Parse the source + let tokens = lex_wfl_with_positions(source); + let mut parser = Parser::new(&tokens); + let program = parser.parse().unwrap(); + + // Test with runtime included (default) + let config = TranspilerConfig { + include_runtime: true, + target: TranspilerTarget::Node, + ..Default::default() + }; + let result = transpile(&program, &config).unwrap(); + assert!(result.code.contains("// WFL Runtime Library")); + assert!(result.code.contains("const WFL = {")); + + // Test with runtime excluded + let config = TranspilerConfig { + include_runtime: false, + target: TranspilerTarget::Node, + ..Default::default() + }; + let result = transpile(&program, &config).unwrap(); + assert!(!result.code.contains("// WFL Runtime Library")); + assert!(!result.code.contains("const WFL = {")); + assert!(result.code.contains("let x = 42")); +} + +#[test] +fn test_pattern_matching_basic() { + let source = r#" + create pattern test_pattern: + "test" + one or more digit + end pattern + + store text as "test123" + if text matches test_pattern then + display "matches" + else + display "no match" + end if + "#; + + let js = transpile_wfl(source).unwrap(); + assert_contains(&js, "new WFL.Pattern"); + println!("Generated JS:\n{}", js); +} + +#[test] +fn test_async_main_function_detection() { + let source = r#" + define action called main: + wait for 1000 milliseconds + display "async main" + end action + "#; + + let js = transpile_wfl(source).unwrap(); + // Should detect async main and wrap in IIFE with await + assert_contains(&js, "(async () => { await main(); })()"); + assert_contains(&js, "async function main()"); +} + +#[test] +fn test_wait_for_with_variable_declaration() { + let source = r#" + wait for store result as 42 + display result + "#; + + let js = transpile_wfl(source).unwrap(); + // Should generate proper await for variable declaration + assert_contains(&js, "let result = await 42"); + assert!(!js.contains("await let")); +}