diff --git a/.build_meta.json b/.build_meta.json index 3851c0ef..19770214 100644 --- a/.build_meta.json +++ b/.build_meta.json @@ -1,5 +1,5 @@ { "year": 26, - "month": 2, - "build": 5 + "month": 1, + "build": 56 } \ No newline at end of file diff --git a/.jules/bolt.md b/.jules/bolt.md index 67c74df0..ad0489cd 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,11 +21,3 @@ ## 2026-01-24 - [Avoid Async Box Allocation for Simple Expressions] **Learning:** `evaluate_expression` was wrapping every call in `Box::pin` for async recursion, even for simple arithmetic operations like `1 + 2`. This caused significant overhead in tight loops. **Action:** Implemented `try_evaluate_expression_sync` to recursively evaluate simple expressions (Literals, Variables, Binary/Unary ops) synchronously, bypassing `Box::pin` allocation. This yielded a ~30% performance improvement in arithmetic-heavy loops. - -## 2026-02-05 - [Batch Interpreter Timeout Checks] -**Learning:** Checking `Instant::elapsed()` on every instruction creates significant overhead (15-20%) in tight loops due to syscalls/hardware clock reads. -**Action:** Implemented a batched check using a simple instruction counter (`op_count & 1023 == 0`), only checking the system clock every 1024 operations. This maintains safety (timeouts are still enforced, just with slightly coarser granularity) while significantly reducing per-instruction overhead. - -## 2026-02-18 - [Unified and Optimized Value Equality] -**Learning:** Three different equality implementations existed (`Value::eq`, `Interpreter::is_equal`, `values_equal`), leading to inconsistent behavior (e.g., `[1] == [1]` was false in WFL code but true in Rust `PartialEq`). Additionally, `Value::eq` unconditionally allocated a `HashSet` for cycle detection, penalizing simple primitive comparisons. -**Action:** Optimized `Value::eq` with a fast path for primitives (avoiding allocation) and updated all call sites to use it. This unified equality logic, fixed correctness bugs for containers, and improved performance for primitives. diff --git a/Cargo.lock b/Cargo.lock index b1ee87c7..7904df60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cast" @@ -3239,7 +3239,7 @@ dependencies = [ [[package]] name = "wfl" -version = "26.2.5" +version = "26.1.56" dependencies = [ "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 0059efe3..280e567e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wfl" -version = "26.2.5" +version = "26.1.56" edition = "2024" description = "WFL (WebFirst Language) is a programming language designed to be readable and intuitive using natural language constructs." license = "Apache-2.0" @@ -11,7 +11,7 @@ default-run = "wfl" name = "WFL" identifier = "com.logbie.wfl" icon = ["icons/wfl.png"] -version = "26.2.5" +version = "26.1.56" copyright = "© 2025 Logbie LLC" category = "Developer Tool" short_description = "WebFirst Language Compiler and Runtime" @@ -50,7 +50,7 @@ sqlx = { version = "0.8.1", features = ["runtime-tokio-rustls", "sqlite", "mysql serde_json = "1.0.114" warp = "0.3.7" uuid = { version = "1.6.1", features = ["v4"] } -bytes = "1.11.1" +bytes = "1.5.0" codespan-reporting = "0.11.1" simplelog = "0.12.1" chrono = "0.4.31" diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 5ae1fb6b..794d2bfe 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -1323,7 +1323,7 @@ impl Analyzer { let server_symbol = Symbol { name: server_name.clone(), kind: SymbolKind::Variable { mutable: false }, - symbol_type: Some(Type::Text), // Server is represented as text + symbol_type: Some(Type::Custom("Server".to_string())), // Server is represented as a custom Server type line: *line, column: *column, }; diff --git a/src/interpreter/assertion_helpers.rs b/src/interpreter/assertion_helpers.rs index b00c4e70..4bfc22c4 100644 --- a/src/interpreter/assertion_helpers.rs +++ b/src/interpreter/assertion_helpers.rs @@ -254,7 +254,25 @@ impl Interpreter { /// Helper function to check if two values are equal fn values_equal(a: &Value, b: &Value) -> bool { - a == b + match (a, b) { + (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON, + (Value::Text(a), Value::Text(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Null, Value::Null) => true, + (Value::Nothing, Value::Nothing) => true, + (Value::List(a), Value::List(b)) => { + let a_ref = a.borrow(); + let b_ref = b.borrow(); + if a_ref.len() != b_ref.len() { + return false; + } + a_ref + .iter() + .zip(b_ref.iter()) + .all(|(x, y)| values_equal(x, y)) + } + _ => false, + } } /// Helper function to check if a value is truthy diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 943d888e..097ed985 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -44,7 +44,7 @@ use crate::parser::ast::{ }; use crate::pattern::CompiledPattern; use crate::stdlib; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::collections::HashMap; use std::io::{self, Write}; use std::net::IpAddr; @@ -341,7 +341,6 @@ pub struct Interpreter { current_count: RefCell>, in_count_loop: RefCell, in_main_loop: RefCell, // Track if we're in a main loop (disables timeout) - op_count: Cell, // Instruction counter for optimized timeout checks started: Instant, max_duration: Duration, call_stack: RefCell>, @@ -1177,7 +1176,6 @@ impl Interpreter { current_count: RefCell::new(None), in_count_loop: RefCell::new(false), in_main_loop: RefCell::new(false), - op_count: Cell::new(0), started: Instant::now(), max_duration: Duration::from_secs(config.timeout_seconds), call_stack: RefCell::new(Vec::new()), @@ -1431,15 +1429,6 @@ impl Interpreter { return Ok(()); } - // Optimization: Only check system time every 1024 operations - // This avoids expensive syscalls/hardware clock reads in tight loops - let count = self.op_count.get(); - self.op_count.set(count.wrapping_add(1)); - - if count & 1023 != 0 { - return Ok(()); - } - if self.started.elapsed() > self.max_duration { if *self.in_count_loop.borrow() { *self.in_count_loop.borrow_mut() = false; @@ -7001,7 +6990,13 @@ impl Interpreter { } fn is_equal(&self, left: &Value, right: &Value) -> bool { - left == right + match (left, right) { + (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON, + (Value::Text(a), Value::Text(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Null, Value::Null) => true, + (a, b) => a == b, + } } // Helper method to create container instance with inheritance diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 48759991..5a78b513 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -3,7 +3,7 @@ use super::error::RuntimeError; use crate::parser::ast::Statement; use crate::pattern::CompiledPattern; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt; use std::rc::{Rc, Weak}; @@ -370,158 +370,25 @@ impl fmt::Display for Value { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { - // Fast path for simple types that don't need cycle detection match (self, other) { - (Value::Number(a), Value::Number(b)) => return (a - b).abs() < f64::EPSILON, - (Value::Text(a), Value::Text(b)) => return a == b, - (Value::Bool(a), Value::Bool(b)) => return a == b, - (Value::Null, Value::Null) => return true, - (Value::Nothing, Value::Nothing) => return true, - (Value::Date(a), Value::Date(b)) => return a == b, - (Value::Time(a), Value::Time(b)) => return a == b, - (Value::DateTime(a), Value::DateTime(b)) => return a == b, - (Value::Pattern(a), Value::Pattern(b)) => return Rc::ptr_eq(a, b), - // For types that might contain cycles or require deeper inspection, use the full visited check - _ => {} - } - - let mut visited = HashSet::new(); - eq_with_visited(self, other, &mut visited) - } -} - -fn eq_with_visited( - lhs: &Value, - rhs: &Value, - visited: &mut HashSet<(*const (), *const ())>, -) -> bool { - match (lhs, rhs) { - (Value::Number(a), Value::Number(b)) => a == b, - (Value::Text(a), Value::Text(b)) => a == b, - (Value::Bool(a), Value::Bool(b)) => a == b, - (Value::Date(a), Value::Date(b)) => a == b, - (Value::Time(a), Value::Time(b)) => a == b, - (Value::DateTime(a), Value::DateTime(b)) => a == b, - (Value::Null, Value::Null) => true, - (Value::Nothing, Value::Nothing) => true, - - (Value::List(a), Value::List(b)) => { - if Rc::ptr_eq(a, b) { - return true; - } - - let ptr_a = Rc::as_ptr(a) as *const (); - let ptr_b = Rc::as_ptr(b) as *const (); - let pair = (ptr_a, ptr_b); - - if visited.contains(&pair) { - return true; // Cycle detected, assume equal for now - } - visited.insert(pair); - - // Use try_borrow to avoid panics if already borrowed mutably - match (a.try_borrow(), b.try_borrow()) { - (Ok(a_ref), Ok(b_ref)) => { - if a_ref.len() != b_ref.len() { - return false; - } - a_ref - .iter() - .zip(b_ref.iter()) - .all(|(x, y)| eq_with_visited(x, y, visited)) - } - _ => false, // Cannot compare if mutably borrowed elsewhere - } - } - - (Value::Object(a), Value::Object(b)) => { - if Rc::ptr_eq(a, b) { - return true; - } - - let ptr_a = Rc::as_ptr(a) as *const (); - let ptr_b = Rc::as_ptr(b) as *const (); - let pair = (ptr_a, ptr_b); - - if visited.contains(&pair) { - return true; - } - visited.insert(pair); - - match (a.try_borrow(), b.try_borrow()) { - (Ok(a_ref), Ok(b_ref)) => { - if a_ref.len() != b_ref.len() { - return false; - } - a_ref.iter().all(|(k, v)| { - b_ref - .get(k) - .is_some_and(|bv| eq_with_visited(v, bv, visited)) - }) - } - _ => false, - } - } - - (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b), - (Value::NativeFunction(name_a, func_a), Value::NativeFunction(name_b, func_b)) => { - name_a == name_b && std::ptr::fn_addr_eq(*func_a, *func_b) - } - (Value::Future(a), Value::Future(b)) => Rc::ptr_eq(a, b), - (Value::Pattern(a), Value::Pattern(b)) => Rc::ptr_eq(a, b), - - (Value::ContainerDefinition(a), Value::ContainerDefinition(b)) => a.name == b.name, - (Value::ContainerInstance(a), Value::ContainerInstance(b)) => { - if Rc::ptr_eq(a, b) { - return true; - } - - let ptr_a = Rc::as_ptr(a) as *const (); - let ptr_b = Rc::as_ptr(b) as *const (); - let pair = (ptr_a, ptr_b); - - if visited.contains(&pair) { - return true; - } - visited.insert(pair); - - match (a.try_borrow(), b.try_borrow()) { - (Ok(a_ref), Ok(b_ref)) => { - if a_ref.container_type != b_ref.container_type { - return false; - } - - // Compare parent hierarchy - let parents_match = match (&a_ref.parent, &b_ref.parent) { - (Some(p1), Some(p2)) => { - let v1 = Value::ContainerInstance(Rc::clone(p1)); - let v2 = Value::ContainerInstance(Rc::clone(p2)); - eq_with_visited(&v1, &v2, visited) - } - (None, None) => true, - _ => false, - }; - - if !parents_match { - return false; - } - - if a_ref.properties.len() != b_ref.properties.len() { - return false; - } - a_ref.properties.iter().all(|(k, v)| { - b_ref - .properties - .get(k) - .is_some_and(|bv| eq_with_visited(v, bv, visited)) - }) - } - _ => false, + (Value::Number(a), Value::Number(b)) => a == b, + (Value::Text(a), Value::Text(b)) => a == b, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Date(a), Value::Date(b)) => a == b, + (Value::Time(a), Value::Time(b)) => a == b, + (Value::DateTime(a), Value::DateTime(b)) => a == b, + (Value::Null, Value::Null) => true, + (Value::Nothing, Value::Nothing) => true, + (Value::ContainerDefinition(a), Value::ContainerDefinition(b)) => a.name == b.name, + (Value::ContainerInstance(a), Value::ContainerInstance(b)) => { + let a = a.borrow(); + let b = b.borrow(); + a.container_type == b.container_type } + (Value::ContainerMethod(a), Value::ContainerMethod(b)) => a.name == b.name, + (Value::ContainerEvent(a), Value::ContainerEvent(b)) => a.name == b.name, + (Value::InterfaceDefinition(a), Value::InterfaceDefinition(b)) => a.name == b.name, + _ => false, } - (Value::ContainerMethod(a), Value::ContainerMethod(b)) => a.name == b.name, - (Value::ContainerEvent(a), Value::ContainerEvent(b)) => a.name == b.name, - (Value::InterfaceDefinition(a), Value::InterfaceDefinition(b)) => a.name == b.name, - _ => false, } } diff --git a/src/stdlib/core.rs b/src/stdlib/core.rs index 1ee96a78..2abefacf 100644 --- a/src/stdlib/core.rs +++ b/src/stdlib/core.rs @@ -1,4 +1,3 @@ -use super::helpers::check_arg_count; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; @@ -16,14 +15,26 @@ pub fn native_print(args: Vec) -> Result { } pub fn native_typeof(args: Vec) -> Result { - check_arg_count("typeof", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("typeof expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let type_name = args[0].type_name(); Ok(Value::Text(Rc::from(type_name))) } pub fn native_isnothing(args: Vec) -> Result { - check_arg_count("isnothing", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("isnothing expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } match &args[0] { Value::Null => Ok(Value::Bool(true)), diff --git a/src/stdlib/crypto.rs b/src/stdlib/crypto.rs index 63e056a7..b97277d9 100644 --- a/src/stdlib/crypto.rs +++ b/src/stdlib/crypto.rs @@ -1,4 +1,3 @@ -use super::helpers::{check_arg_count, expect_text}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; @@ -418,52 +417,121 @@ fn bytes_to_hex(bytes: &[u8]) -> String { /// WFLHASH-256 implementation with security fixes pub fn native_wflhash256(args: Vec) -> Result { - check_arg_count("wflhash256", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + "Invalid argument count".to_string(), + 0, + 0, + )); + } + + let input = match &args[0] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; - let text = expect_text(&args[0])?; let params = WflHashParams::new(32); // 256 bits = 32 bytes - let hash = wflhash_core_text(text.as_bytes(), ¶ms)?; - Ok(Value::Text(Rc::from(bytes_to_hex(&hash)))) + let hash_bytes = wflhash_core_text(input, ¶ms)?; // Validate UTF-8 for text + let hash_hex = bytes_to_hex(&hash_bytes); + + Ok(Value::Text(Rc::from(hash_hex))) } /// WFLHASH-512 implementation with security fixes pub fn native_wflhash512(args: Vec) -> Result { - check_arg_count("wflhash512", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + "Invalid argument count".to_string(), + 0, + 0, + )); + } + + let input = match &args[0] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; - let text = expect_text(&args[0])?; let params = WflHashParams::new(64); // 512 bits = 64 bytes - let hash = wflhash_core_text(text.as_bytes(), ¶ms)?; - Ok(Value::Text(Rc::from(bytes_to_hex(&hash)))) + let hash_bytes = wflhash_core_text(input, ¶ms)?; // Validate UTF-8 for text + let hash_hex = bytes_to_hex(&hash_bytes); + + Ok(Value::Text(Rc::from(hash_hex))) } /// WFLHASH-256 with personalization/salt support pub fn native_wflhash256_with_salt(args: Vec) -> Result { - check_arg_count("wflhash256_with_salt", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + "Invalid argument count".to_string(), + 0, + 0, + )); + } + + let input = match &args[0] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; - let text = expect_text(&args[0])?; - let salt = expect_text(&args[1])?; - let params = WflHashParams::new_with_personalization(32, salt.as_bytes()); - let hash = wflhash_core_text(text.as_bytes(), ¶ms)?; - Ok(Value::Text(Rc::from(bytes_to_hex(&hash)))) + let salt = match &args[1] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; + + let params = WflHashParams::new_with_personalization(32, salt); + let hash_bytes = wflhash_core_text(input, ¶ms)?; + let hash_hex = bytes_to_hex(&hash_bytes); + + Ok(Value::Text(Rc::from(hash_hex))) } /// WFLHASH-256 with key for MAC functionality (WFLMAC-256) /// Now uses proper HKDF key derivation for enhanced security pub fn native_wflmac256(args: Vec) -> Result { - check_arg_count("wflmac256", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + "Invalid argument count".to_string(), + 0, + 0, + )); + } + + let input = match &args[0] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; - let text = expect_text(&args[0])?; - let key = expect_text(&args[1])?; - let params = WflHashParams::new_with_key(32, key.as_bytes())?; - let hash = wflhash_core_text(text.as_bytes(), ¶ms)?; - Ok(Value::Text(Rc::from(bytes_to_hex(&hash)))) + let key = match &args[1] { + Value::Text(text) => text.as_bytes(), + _ => { + return Err(RuntimeError::new("Invalid argument type".to_string(), 0, 0)); + } + }; + + // Use proper key derivation with error handling + let params = WflHashParams::new_with_key(32, key)?; + let hash_bytes = wflhash_core_text(input, ¶ms)?; + let hash_hex = bytes_to_hex(&hash_bytes); + + Ok(Value::Text(Rc::from(hash_hex))) } /// WFLHASH-256 for binary data (no UTF-8 validation) pub fn native_wflhash256_binary(data: &[u8]) -> Result { let params = WflHashParams::new(32); // 256 bits = 32 bytes - let hash = wflhash_core(data, ¶ms)?; - Ok(bytes_to_hex(&hash)) + let hash_bytes = wflhash_core(data, ¶ms)?; + Ok(bytes_to_hex(&hash_bytes)) } /// Constant-time MAC verification using subtle crate @@ -489,9 +557,7 @@ pub fn wflmac256_verify( /// Generate a cryptographically secure random token (for CSRF, sessions, etc.) /// Usage: generate_csrf_token() -> "a1b2c3d4e5f6..." -pub fn native_generate_csrf_token(args: Vec) -> Result { - check_arg_count("generate_csrf_token", &args, 0)?; - +pub fn native_generate_csrf_token(_args: Vec) -> Result { use rand::RngCore; // Generate 32 random bytes (256 bits) diff --git a/src/stdlib/filesystem.rs b/src/stdlib/filesystem.rs index 543430eb..eb877d83 100644 --- a/src/stdlib/filesystem.rs +++ b/src/stdlib/filesystem.rs @@ -1,4 +1,3 @@ -use super::helpers::{check_arg_count, check_arg_range, expect_text}; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; use std::cell::RefCell; @@ -6,11 +5,28 @@ use std::fs; use std::path::{Path, PathBuf}; use std::rc::Rc; +fn expect_text(value: &Value) -> Result<&str, RuntimeError> { + match value { + Value::Text(text) => Ok(text), + _ => Err(RuntimeError::new( + format!("Expected text, got {}", value.type_name()), + 0, + 0, + )), + } +} + pub fn native_list_dir(args: Vec) -> Result { - check_arg_count("list_dir", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("list_dir expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); if !path.exists() { return Err(RuntimeError::new( @@ -46,7 +62,13 @@ pub fn native_list_dir(args: Vec) -> Result { } pub fn native_glob(args: Vec) -> Result { - check_arg_count("glob", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("glob expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let pattern = expect_text(&args[0])?; let base_path = expect_text(&args[1])?; @@ -74,7 +96,13 @@ pub fn native_glob(args: Vec) -> Result { } pub fn native_rglob(args: Vec) -> Result { - check_arg_count("rglob", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("rglob expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let pattern = expect_text(&args[0])?; let base_path = expect_text(&args[1])?; @@ -107,14 +135,18 @@ pub fn native_rglob(args: Vec) -> Result { } pub fn native_path_join(args: Vec) -> Result { - // check_min_arg_count is needed here because it says "expects at least 1 argument" - // But helper implementation uses check_min_arg_count - super::helpers::check_min_arg_count("path_join", &args, 1)?; + if args.is_empty() { + return Err(RuntimeError::new( + "path_join expects at least 1 argument".to_string(), + 0, + 0, + )); + } let mut path = PathBuf::new(); for arg in &args { let component = expect_text(arg)?; - path.push(component.as_ref()); + path.push(component); } let result = path.to_string_lossy(); @@ -122,10 +154,16 @@ pub fn native_path_join(args: Vec) -> Result { } pub fn native_path_basename(args: Vec) -> Result { - check_arg_count("path_basename", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("path_basename expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); let basename = path .file_name() @@ -136,10 +174,16 @@ pub fn native_path_basename(args: Vec) -> Result { } pub fn native_path_dirname(args: Vec) -> Result { - check_arg_count("path_dirname", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("path_dirname expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); let dirname = path .parent() @@ -150,10 +194,16 @@ pub fn native_path_dirname(args: Vec) -> Result { } pub fn native_makedirs(args: Vec) -> Result { - check_arg_count("makedirs", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("makedirs expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); fs::create_dir_all(path).map_err(|e| { RuntimeError::new( @@ -167,10 +217,16 @@ pub fn native_makedirs(args: Vec) -> Result { } pub fn native_file_mtime(args: Vec) -> Result { - check_arg_count("file_mtime", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("file_mtime expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); if !path.exists() { return Err(RuntimeError::new( @@ -210,37 +266,61 @@ pub fn native_file_mtime(args: Vec) -> Result { } pub fn native_path_exists(args: Vec) -> Result { - check_arg_count("path_exists", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("path_exists expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); Ok(Value::Bool(path.exists())) } pub fn native_is_file(args: Vec) -> Result { - check_arg_count("is_file", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("is_file expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); Ok(Value::Bool(path.is_file())) } pub fn native_is_dir(args: Vec) -> Result { - check_arg_count("is_dir", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("is_dir expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); Ok(Value::Bool(path.is_dir())) } pub fn native_count_lines(args: Vec) -> Result { - check_arg_count("count_lines", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("count_lines expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); if !path.exists() { return Err(RuntimeError::new( @@ -280,10 +360,16 @@ pub fn native_count_lines(args: Vec) -> Result { } pub fn native_path_extension(args: Vec) -> Result { - check_arg_count("path_extension", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("path_extension expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); @@ -291,10 +377,16 @@ pub fn native_path_extension(args: Vec) -> Result { } pub fn native_path_stem(args: Vec) -> Result { - check_arg_count("path_stem", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("path_stem expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); @@ -302,10 +394,16 @@ pub fn native_path_stem(args: Vec) -> Result { } pub fn native_file_size(args: Vec) -> Result { - check_arg_count("file_size", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("file_size expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); if !path.exists() { return Err(RuntimeError::new( @@ -335,12 +433,18 @@ pub fn native_file_size(args: Vec) -> Result { } pub fn native_copy_file(args: Vec) -> Result { - check_arg_count("copy_file", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("copy_file expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let source_str = expect_text(&args[0])?; let dest_str = expect_text(&args[1])?; - let source = Path::new(source_str.as_ref()); - let dest = Path::new(dest_str.as_ref()); + let source = Path::new(source_str); + let dest = Path::new(dest_str); if !source.exists() { return Err(RuntimeError::new( @@ -370,12 +474,18 @@ pub fn native_copy_file(args: Vec) -> Result { } pub fn native_move_file(args: Vec) -> Result { - check_arg_count("move_file", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("move_file expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let source_str = expect_text(&args[0])?; let dest_str = expect_text(&args[1])?; - let source = Path::new(source_str.as_ref()); - let dest = Path::new(dest_str.as_ref()); + let source = Path::new(source_str); + let dest = Path::new(dest_str); if !source.exists() { return Err(RuntimeError::new( @@ -397,10 +507,16 @@ pub fn native_move_file(args: Vec) -> Result { } pub fn native_remove_file(args: Vec) -> Result { - check_arg_count("remove_file", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("remove_file expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); if !path.exists() { return Err(RuntimeError::new( @@ -425,10 +541,16 @@ pub fn native_remove_file(args: Vec) -> Result { } pub fn native_remove_dir(args: Vec) -> Result { - check_arg_range("remove_dir", &args, 1, 2)?; + if args.is_empty() || args.len() > 2 { + return Err(RuntimeError::new( + format!("remove_dir expects 1 or 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let path_str = expect_text(&args[0])?; - let path = Path::new(path_str.as_ref()); + let path = Path::new(path_str); // Check for optional recursive parameter let recursive = if args.len() == 2 { @@ -567,7 +689,7 @@ mod tests { let value = Value::Text(Rc::from("test")); let result = expect_text(&value); assert!(result.is_ok()); - assert_eq!(result.unwrap().as_ref(), "test"); + assert_eq!(result.unwrap(), "test"); } #[test] diff --git a/src/stdlib/helpers.rs b/src/stdlib/helpers.rs deleted file mode 100644 index 8258f1a1..00000000 --- a/src/stdlib/helpers.rs +++ /dev/null @@ -1,435 +0,0 @@ -use crate::interpreter::error::RuntimeError; -use crate::interpreter::value::Value; -use std::cell::RefCell; -use std::rc::Rc; - -/// Validates that a native function receives exactly the expected number of arguments. -/// -/// This is the most common argument validation helper, used when a function requires -/// a specific number of arguments (not a range or minimum). The error message -/// automatically handles singular/plural grammar for better user experience. -/// -/// # Arguments -/// -/// * `func_name` - The name of the function being validated (used in error messages) -/// * `args` - The slice of argument values to check -/// * `expected` - The exact number of arguments required -/// -/// # Returns -/// -/// Returns `Ok(())` if the argument count matches, allowing the function to proceed. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the argument count doesn't match the expected value. -/// The error message format is: "{func_name} expects {expected} argument(s), got {actual}" -/// with proper singular/plural handling. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_add(args: Vec) -> Result { -/// check_arg_count("add", &args, 2)?; // Requires exactly 2 arguments -/// // ... function implementation -/// } -/// ``` -pub fn check_arg_count( - func_name: &str, - args: &[Value], - expected: usize, -) -> Result<(), RuntimeError> { - if args.len() != expected { - return Err(RuntimeError::new( - format!( - "{} expects {} argument{}, got {}", - func_name, - expected, - if expected == 1 { "" } else { "s" }, - args.len() - ), - 0, - 0, - )); - } - Ok(()) -} - -/// Validates that a native function receives at least the minimum number of arguments. -/// -/// Use this helper for variadic functions that accept a minimum number of required -/// arguments plus optional additional arguments. This is common for functions like -/// print, format, or concatenation operations that can handle variable inputs. -/// -/// # Arguments -/// -/// * `func_name` - The name of the function being validated (used in error messages) -/// * `args` - The slice of argument values to check -/// * `min_count` - The minimum number of arguments required -/// -/// # Returns -/// -/// Returns `Ok(())` if the argument count is at least `min_count`. -/// -/// # Errors -/// -/// Returns `RuntimeError` if fewer than `min_count` arguments are provided. -/// The error message format is: "{func_name} expects at least {min_count} argument(s), got {actual}" -/// with proper singular/plural handling. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_print(args: Vec) -> Result { -/// check_min_arg_count("print", &args, 1)?; // Requires at least 1 argument -/// // ... can process args.len() arguments -/// } -/// ``` -pub fn check_min_arg_count( - func_name: &str, - args: &[Value], - min_count: usize, -) -> Result<(), RuntimeError> { - if args.len() < min_count { - return Err(RuntimeError::new( - format!( - "{} expects at least {} argument{}, got {}", - func_name, - min_count, - if min_count == 1 { "" } else { "s" }, - args.len() - ), - 0, - 0, - )); - } - Ok(()) -} - -/// Validates that a native function receives an argument count within a specified range. -/// -/// Use this helper for functions that accept a flexible number of arguments within -/// bounds, such as functions with multiple optional parameters. The range is inclusive -/// on both ends: [min, max]. -/// -/// # Arguments -/// -/// * `func_name` - The name of the function being validated (used in error messages) -/// * `args` - The slice of argument values to check -/// * `min` - The minimum number of arguments allowed (inclusive) -/// * `max` - The maximum number of arguments allowed (inclusive) -/// -/// # Returns -/// -/// Returns `Ok(())` if the argument count is within [min, max] (inclusive). -/// -/// # Errors -/// -/// Returns `RuntimeError` if the argument count is outside the specified range. -/// The error message format is: "{func_name} expects between {min} and {max} arguments, got {actual}". -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_substring(args: Vec) -> Result { -/// check_arg_range("substring", &args, 2, 3)?; // Requires 2 or 3 arguments -/// // ... handle optional third argument -/// } -/// ``` -pub fn check_arg_range( - func_name: &str, - args: &[Value], - min: usize, - max: usize, -) -> Result<(), RuntimeError> { - if args.len() < min || args.len() > max { - return Err(RuntimeError::new( - format!( - "{} expects between {} and {} arguments, got {}", - func_name, - min, - max, - args.len() - ), - 0, - 0, - )); - } - Ok(()) -} - -/// Extracts a number value from a WFL Value, returning it as a primitive f64. -/// -/// This is the most common type extractor for numeric operations. Returns a copy -/// of the f64 value rather than a reference since f64 implements Copy. -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns the f64 number if the value is a Number variant. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a Number, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_abs(args: Vec) -> Result { -/// check_arg_count("abs", &args, 1)?; -/// let num = expect_number(&args[0])?; -/// Ok(Value::Number(num.abs())) -/// } -/// ``` -pub fn expect_number(value: &Value) -> Result { - match value { - Value::Number(n) => Ok(*n), - _ => Err(RuntimeError::new( - format!("Expected a number, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a text value from a WFL Value, returning it as a reference-counted string. -/// -/// Returns an `Rc` to enable efficient memory sharing without copying the string -/// data. This is the standard way to extract text values in the WFL runtime. -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns an `Rc` clone (incrementing the reference count) if the value is a Text variant. -/// The underlying string data is not copied, only the reference count is incremented. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a Text, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_uppercase(args: Vec) -> Result { -/// check_arg_count("uppercase", &args, 1)?; -/// let text = expect_text(&args[0])?; -/// Ok(Value::Text(Rc::from(text.to_uppercase()))) -/// } -/// ``` -pub fn expect_text(value: &Value) -> Result, RuntimeError> { - match value { - Value::Text(s) => Ok(Rc::clone(s)), - _ => Err(RuntimeError::new( - format!("Expected text, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a list value from a WFL Value, returning it as a reference-counted mutable vector. -/// -/// Returns an `Rc>>` to enable efficient memory sharing with interior -/// mutability. The RefCell allows mutation of the list contents even through shared references, -/// which is essential for list operations like push, pop, and element modification. -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns an `Rc>>` clone (incrementing the reference count) if the value -/// is a List variant. Multiple references to the same list share the underlying data. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a List, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_push(args: Vec) -> Result { -/// check_arg_count("push", &args, 2)?; -/// let list = expect_list(&args[0])?; -/// list.borrow_mut().push(args[1].clone()); -/// Ok(Value::Nothing) -/// } -/// ``` -pub fn expect_list(value: &Value) -> Result>>, RuntimeError> { - match value { - Value::List(list) => Ok(Rc::clone(list)), - _ => Err(RuntimeError::new( - format!("Expected a list, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a boolean value from a WFL Value, returning it as a primitive bool. -/// -/// Returns a copy of the bool value rather than a reference since bool implements Copy. -/// This is commonly used in conditional logic and boolean operations. -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns the bool if the value is a Bool variant. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a Bool, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_not(args: Vec) -> Result { -/// check_arg_count("not", &args, 1)?; -/// let b = expect_bool(&args[0])?; -/// Ok(Value::Bool(!b)) -/// } -/// ``` -pub fn expect_bool(value: &Value) -> Result { - match value { - Value::Bool(b) => Ok(*b), - _ => Err(RuntimeError::new( - format!("Expected a boolean, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a Date value from a WFL Value, returning it as a reference-counted NaiveDate. -/// -/// Returns an `Rc` to enable efficient memory sharing of date values. -/// NaiveDate represents a date without timezone information (year, month, day only). -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns an `Rc` clone (incrementing the reference count) if the value -/// is a Date variant. The underlying date data is shared, not copied. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a Date, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_date_year(args: Vec) -> Result { -/// check_arg_count("date_year", &args, 1)?; -/// let date = expect_date(&args[0])?; -/// Ok(Value::Number(date.year() as f64)) -/// } -/// ``` -pub fn expect_date(value: &Value) -> Result, RuntimeError> { - match value { - Value::Date(d) => Ok(Rc::clone(d)), - _ => Err(RuntimeError::new( - format!("Expected a Date, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a Time value from a WFL Value, returning it as a reference-counted NaiveTime. -/// -/// Returns an `Rc` to enable efficient memory sharing of time values. -/// NaiveTime represents a time of day without timezone information (hour, minute, second, nanosecond). -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns an `Rc` clone (incrementing the reference count) if the value -/// is a Time variant. The underlying time data is shared, not copied. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a Time, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_time_hour(args: Vec) -> Result { -/// check_arg_count("time_hour", &args, 1)?; -/// let time = expect_time(&args[0])?; -/// Ok(Value::Number(time.hour() as f64)) -/// } -/// ``` -pub fn expect_time(value: &Value) -> Result, RuntimeError> { - match value { - Value::Time(t) => Ok(Rc::clone(t)), - _ => Err(RuntimeError::new( - format!("Expected a Time, got {}", value.type_name()), - 0, - 0, - )), - } -} - -/// Extracts a DateTime value from a WFL Value, returning it as a reference-counted NaiveDateTime. -/// -/// Returns an `Rc` to enable efficient memory sharing of datetime values. -/// NaiveDateTime represents a date and time without timezone information, combining both -/// date (year, month, day) and time (hour, minute, second, nanosecond) components. -/// -/// # Arguments -/// -/// * `value` - The WFL Value to extract from -/// -/// # Returns -/// -/// Returns an `Rc` clone (incrementing the reference count) if the value -/// is a DateTime variant. The underlying datetime data is shared, not copied. -/// -/// # Errors -/// -/// Returns `RuntimeError` if the value is not a DateTime, with an error message -/// indicating the expected type and the actual type received. -/// -/// # Examples -/// -/// ```ignore -/// pub fn native_datetime_add_days(args: Vec) -> Result { -/// check_arg_count("datetime_add_days", &args, 2)?; -/// let dt = expect_datetime(&args[0])?; -/// let days = expect_number(&args[1])? as i64; -/// let new_dt = dt.checked_add_signed(Duration::days(days)) -/// .ok_or_else(|| RuntimeError::new("Date overflow".to_string(), 0, 0))?; -/// Ok(Value::DateTime(Rc::new(new_dt))) -/// } -/// ``` -pub fn expect_datetime(value: &Value) -> Result, RuntimeError> { - match value { - Value::DateTime(dt) => Ok(Rc::clone(dt)), - _ => Err(RuntimeError::new( - format!("Expected a DateTime, got {}", value.type_name()), - 0, - 0, - )), - } -} diff --git a/src/stdlib/json.rs b/src/stdlib/json.rs index 47204545..58aca258 100644 --- a/src/stdlib/json.rs +++ b/src/stdlib/json.rs @@ -1,4 +1,3 @@ -use super::helpers::{check_arg_count, expect_text}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; @@ -6,6 +5,17 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +fn expect_text(value: &Value) -> Result, RuntimeError> { + match value { + Value::Text(s) => Ok(Rc::clone(s)), + _ => Err(RuntimeError::new( + format!("Expected text, got {}", value.type_name()), + 0, + 0, + )), + } +} + /// Convert serde_json::Value to WFL Value fn json_to_wfl(json: serde_json::Value) -> Value { match json { @@ -76,7 +86,13 @@ fn wfl_to_json(value: &Value) -> Result { /// Parse JSON string to WFL value /// Usage: parse_json(json_text) pub fn native_parse_json(args: Vec) -> Result { - check_arg_count("parse_json", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("parse_json expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let json_text = expect_text(&args[0])?; @@ -93,7 +109,13 @@ pub fn native_parse_json(args: Vec) -> Result { /// Convert WFL value to JSON string /// Usage: stringify_json(value) pub fn native_stringify_json(args: Vec) -> Result { - check_arg_count("stringify_json", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("stringify_json expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let json_value = wfl_to_json(&args[0])?; @@ -110,7 +132,16 @@ pub fn native_stringify_json(args: Vec) -> Result { /// Convert WFL value to pretty-printed JSON string /// Usage: stringify_json_pretty(value) pub fn native_stringify_json_pretty(args: Vec) -> Result { - check_arg_count("stringify_json_pretty", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!( + "stringify_json_pretty expects 1 argument, got {}", + args.len() + ), + 0, + 0, + )); + } let json_value = wfl_to_json(&args[0])?; diff --git a/src/stdlib/list.rs b/src/stdlib/list.rs index c92793ea..644bae5c 100644 --- a/src/stdlib/list.rs +++ b/src/stdlib/list.rs @@ -1,10 +1,40 @@ -use super::helpers::{check_arg_count, expect_list}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; +use std::cell::RefCell; +use std::rc::Rc; + +fn expect_list(value: &Value) -> Result>>, RuntimeError> { + match value { + Value::List(list) => Ok(Rc::clone(list)), + _ => Err(RuntimeError::new( + format!("Expected a list, got {}", value.type_name()), + 0, + 0, + )), + } +} + +#[allow(dead_code)] +fn expect_number(value: &Value) -> Result { + match value { + Value::Number(n) => Ok(*n), + _ => Err(RuntimeError::new( + format!("Expected a number, got {}", value.type_name()), + 0, + 0, + )), + } +} pub fn native_length(args: Vec) -> Result { - check_arg_count("length", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("length expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } match &args[0] { Value::List(list) => Ok(Value::Number(list.borrow().len() as f64)), @@ -18,7 +48,13 @@ pub fn native_length(args: Vec) -> Result { } pub fn native_push(args: Vec) -> Result { - check_arg_count("push", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("push expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let list = expect_list(&args[0])?; let item = args[1].clone(); @@ -28,7 +64,13 @@ pub fn native_push(args: Vec) -> Result { } pub fn native_pop(args: Vec) -> Result { - check_arg_count("pop", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("pop expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let list = expect_list(&args[0])?; let mut list_ref = list.borrow_mut(); @@ -45,13 +87,19 @@ pub fn native_pop(args: Vec) -> Result { } pub fn native_contains(args: Vec) -> Result { - check_arg_count("contains", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("contains expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let list = expect_list(&args[0])?; let item = &args[1]; for value in list.borrow().iter() { - if value == item { + if format!("{value:?}") == format!("{item:?}") { return Ok(Value::Bool(true)); } } @@ -60,13 +108,19 @@ pub fn native_contains(args: Vec) -> Result { } pub fn native_indexof(args: Vec) -> Result { - check_arg_count("indexof", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("indexof expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let list = expect_list(&args[0])?; let item = &args[1]; for (i, value) in list.borrow().iter().enumerate() { - if value == item { + if format!("{value:?}") == format!("{item:?}") { return Ok(Value::Number(i as f64)); } } diff --git a/src/stdlib/math.rs b/src/stdlib/math.rs index c70f78fd..cd206351 100644 --- a/src/stdlib/math.rs +++ b/src/stdlib/math.rs @@ -1,38 +1,78 @@ -use super::helpers::{check_arg_count, expect_number}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; +fn expect_number(value: &Value) -> Result { + match value { + Value::Number(n) => Ok(*n), + _ => Err(RuntimeError::new( + format!("Expected a number, got {}", value.type_name()), + 0, + 0, + )), + } +} + pub fn native_abs(args: Vec) -> Result { - check_arg_count("abs", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("abs expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let x = expect_number(&args[0])?; Ok(Value::Number(x.abs())) } pub fn native_round(args: Vec) -> Result { - check_arg_count("round", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("round expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let x = expect_number(&args[0])?; Ok(Value::Number(x.round())) } pub fn native_floor(args: Vec) -> Result { - check_arg_count("floor", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("floor expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let x = expect_number(&args[0])?; Ok(Value::Number(x.floor())) } pub fn native_ceil(args: Vec) -> Result { - check_arg_count("ceil", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("ceil expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let x = expect_number(&args[0])?; Ok(Value::Number(x.ceil())) } pub fn native_clamp(args: Vec) -> Result { - check_arg_count("clamp", &args, 3)?; + if args.len() != 3 { + return Err(RuntimeError::new( + format!("clamp expects 3 arguments, got {}", args.len()), + 0, + 0, + )); + } let value = expect_number(&args[0])?; let min = expect_number(&args[1])?; diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index b9e2763d..85af68ff 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -1,7 +1,6 @@ pub mod core; pub mod crypto; pub mod filesystem; -pub mod helpers; pub mod json; pub mod list; pub mod math; diff --git a/src/stdlib/text.rs b/src/stdlib/text.rs index 9b3cea86..a91a3e58 100644 --- a/src/stdlib/text.rs +++ b/src/stdlib/text.rs @@ -1,10 +1,31 @@ -use super::helpers::{check_arg_count, expect_number, expect_text}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; use std::cell::RefCell; use std::rc::Rc; +fn expect_text(value: &Value) -> Result, RuntimeError> { + match value { + Value::Text(s) => Ok(Rc::clone(s)), + _ => Err(RuntimeError::new( + format!("Expected text, got {}", value.type_name()), + 0, + 0, + )), + } +} + +fn expect_number(value: &Value) -> Result { + match value { + Value::Number(n) => Ok(*n), + _ => Err(RuntimeError::new( + format!("Expected a number, got {}", value.type_name()), + 0, + 0, + )), + } +} + /// Decode percent-encoded URL string /// Converts '+' to space and decodes %HH hex sequences /// Invalid sequences are left as-is @@ -73,7 +94,13 @@ fn parse_key_value_pairs(input: &str, delimiter: char) -> std::collections::Hash // which handles both text and lists pub fn native_touppercase(args: Vec) -> Result { - check_arg_count("touppercase", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("touppercase expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let uppercase = text.to_uppercase(); @@ -81,7 +108,13 @@ pub fn native_touppercase(args: Vec) -> Result { } pub fn native_tolowercase(args: Vec) -> Result { - check_arg_count("tolowercase", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("tolowercase expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let lowercase = text.to_lowercase(); @@ -89,7 +122,13 @@ pub fn native_tolowercase(args: Vec) -> Result { } pub fn native_contains(args: Vec) -> Result { - check_arg_count("contains", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("contains expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let substring = expect_text(&args[1])?; @@ -98,7 +137,13 @@ pub fn native_contains(args: Vec) -> Result { } pub fn native_substring(args: Vec) -> Result { - check_arg_count("substring", &args, 3)?; + if args.len() != 3 { + return Err(RuntimeError::new( + format!("substring expects 3 arguments, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let start = expect_number(&args[1])? as usize; @@ -119,7 +164,13 @@ pub fn native_substring(args: Vec) -> Result { } pub fn native_string_split(args: Vec) -> Result { - check_arg_count("string_split", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("string_split expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let delimiter = expect_text(&args[1])?; @@ -143,7 +194,13 @@ pub fn native_string_split(args: Vec) -> Result { } pub fn native_trim(args: Vec) -> Result { - check_arg_count("trim", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("trim expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let trimmed = text.trim(); @@ -151,7 +208,13 @@ pub fn native_trim(args: Vec) -> Result { } pub fn native_starts_with(args: Vec) -> Result { - check_arg_count("starts_with", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("starts_with expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let prefix = expect_text(&args[1])?; @@ -160,7 +223,13 @@ pub fn native_starts_with(args: Vec) -> Result { } pub fn native_ends_with(args: Vec) -> Result { - check_arg_count("ends_with", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("ends_with expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } let text = expect_text(&args[0])?; let suffix = expect_text(&args[1])?; @@ -171,7 +240,13 @@ pub fn native_ends_with(args: Vec) -> Result { /// Parse query string into WFL object /// Usage: parse_query_string("?page=1&limit=10") -> {"page": "1", "limit": "10"} pub fn native_parse_query_string(args: Vec) -> Result { - check_arg_count("parse_query_string", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("parse_query_string expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let query_str = expect_text(&args[0])?; let query_str = query_str.trim_start_matches('?'); @@ -186,7 +261,13 @@ pub fn native_parse_query_string(args: Vec) -> Result) -> Result { use std::collections::HashMap; - check_arg_count("parse_cookies", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!("parse_cookies expects 1 argument, got {}", args.len()), + 0, + 0, + )); + } let cookie_header = expect_text(&args[0])?; let mut cookies = HashMap::new(); @@ -211,7 +292,16 @@ pub fn native_parse_cookies(args: Vec) -> Result { /// Parse URL-encoded form data /// Usage: parse_form_urlencoded("name=Alice&age=30") -> {"name": "Alice", "age": "30"} pub fn native_parse_form_urlencoded(args: Vec) -> Result { - check_arg_count("parse_form_urlencoded", &args, 1)?; + if args.len() != 1 { + return Err(RuntimeError::new( + format!( + "parse_form_urlencoded expects 1 argument, got {}", + args.len() + ), + 0, + 0, + )); + } let form_data = expect_text(&args[0])?; diff --git a/src/stdlib/time.rs b/src/stdlib/time.rs index 0269ad16..8191aa6d 100644 --- a/src/stdlib/time.rs +++ b/src/stdlib/time.rs @@ -1,7 +1,3 @@ -use super::helpers::{ - check_arg_count, check_arg_range, expect_date, expect_datetime, expect_number, expect_text, - expect_time, -}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; @@ -10,7 +6,13 @@ use std::rc::Rc; /// Returns the current date pub fn native_today(args: Vec) -> Result { - check_arg_count("today", &args, 0)?; + if !args.is_empty() { + return Err(RuntimeError::new( + format!("today expects 0 arguments, got {}", args.len()), + 0, + 0, + )); + } let today = Local::now().date_naive(); Ok(Value::Date(Rc::new(today))) @@ -18,7 +20,13 @@ pub fn native_today(args: Vec) -> Result { /// Returns the current time pub fn native_now(args: Vec) -> Result { - check_arg_count("now", &args, 0)?; + if !args.is_empty() { + return Err(RuntimeError::new( + format!("now expects 0 arguments, got {}", args.len()), + 0, + 0, + )); + } let now = Local::now().time(); Ok(Value::Time(Rc::new(now))) @@ -26,7 +34,13 @@ pub fn native_now(args: Vec) -> Result { /// Returns the current date and time pub fn native_datetime_now(args: Vec) -> Result { - check_arg_count("datetime_now", &args, 0)?; + if !args.is_empty() { + return Err(RuntimeError::new( + format!("datetime_now expects 0 arguments, got {}", args.len()), + 0, + 0, + )); + } let now = Local::now().naive_local(); Ok(Value::DateTime(Rc::new(now))) @@ -34,10 +48,41 @@ pub fn native_datetime_now(args: Vec) -> Result { /// Formats a date according to a format string pub fn native_format_date(args: Vec) -> Result { - check_arg_count("format_date", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("format_date expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } + + let date = match &args[0] { + Value::Date(d) => d.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_date expects a Date as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; - let date = expect_date(&args[0])?; - let format_string = expect_text(&args[1])?; + let format_string = match &args[1] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_date expects a Text as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let formatted = date.format(&format_string).to_string(); Ok(Value::Text(formatted.into())) @@ -45,10 +90,41 @@ pub fn native_format_date(args: Vec) -> Result { /// Formats a time according to a format string pub fn native_format_time(args: Vec) -> Result { - check_arg_count("format_time", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("format_time expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } + + let time = match &args[0] { + Value::Time(t) => t.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_time expects a Time as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; - let time = expect_time(&args[0])?; - let format_string = expect_text(&args[1])?; + let format_string = match &args[1] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_time expects a Text as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let formatted = time.format(&format_string).to_string(); Ok(Value::Text(formatted.into())) @@ -56,10 +132,41 @@ pub fn native_format_time(args: Vec) -> Result { /// Formats a datetime according to a format string pub fn native_format_datetime(args: Vec) -> Result { - check_arg_count("format_datetime", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("format_datetime expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } - let datetime = expect_datetime(&args[0])?; - let format_string = expect_text(&args[1])?; + let datetime = match &args[0] { + Value::DateTime(dt) => dt.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_datetime expects a DateTime as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; + + let format_string = match &args[1] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "format_datetime expects a Text as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let formatted = datetime.format(&format_string).to_string(); Ok(Value::Text(formatted.into())) @@ -67,10 +174,41 @@ pub fn native_format_datetime(args: Vec) -> Result { /// Parses a date from a string pub fn native_parse_date(args: Vec) -> Result { - check_arg_count("parse_date", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("parse_date expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } - let date_str = expect_text(&args[0])?; - let format_string = expect_text(&args[1])?; + let date_str = match &args[0] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "parse_date expects a Text as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; + + let format_string = match &args[1] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "parse_date expects a Text as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; match NaiveDate::parse_from_str(&date_str, &format_string) { Ok(date) => Ok(Value::Date(Rc::new(date))), @@ -84,10 +222,41 @@ pub fn native_parse_date(args: Vec) -> Result { /// Parses a time from a string pub fn native_parse_time(args: Vec) -> Result { - check_arg_count("parse_time", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("parse_time expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } + + let time_str = match &args[0] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "parse_time expects a Text as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; - let time_str = expect_text(&args[0])?; - let format_string = expect_text(&args[1])?; + let format_string = match &args[1] { + Value::Text(s) => s.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "parse_time expects a Text as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; match NaiveTime::parse_from_str(&time_str, &format_string) { Ok(time) => Ok(Value::Time(Rc::new(time))), @@ -101,13 +270,56 @@ pub fn native_parse_time(args: Vec) -> Result { /// Creates a time from hours, minutes, and seconds pub fn native_create_time(args: Vec) -> Result { - check_arg_range("create_time", &args, 2, 3)?; + if args.len() < 2 || args.len() > 3 { + return Err(RuntimeError::new( + format!("create_time expects 2 or 3 arguments, got {}", args.len()), + 0, + 0, + )); + } + + let hours = match &args[0] { + Value::Number(n) => *n as u32, + _ => { + return Err(RuntimeError::new( + format!( + "create_time expects a Number as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; - let hours = expect_number(&args[0])? as u32; - let minutes = expect_number(&args[1])? as u32; + let minutes = match &args[1] { + Value::Number(n) => *n as u32, + _ => { + return Err(RuntimeError::new( + format!( + "create_time expects a Number as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let seconds = if args.len() == 3 { - expect_number(&args[2])? as u32 + match &args[2] { + Value::Number(n) => *n as u32, + _ => { + return Err(RuntimeError::new( + format!( + "create_time expects a Number as third argument, got {}", + args[2].type_name() + ), + 0, + 0, + )); + } + } } else { 0 }; @@ -150,11 +362,55 @@ pub fn native_create_time(args: Vec) -> Result { /// Creates a date from year, month, and day pub fn native_create_date(args: Vec) -> Result { - check_arg_count("create_date", &args, 3)?; + if args.len() != 3 { + return Err(RuntimeError::new( + format!("create_date expects 3 arguments, got {}", args.len()), + 0, + 0, + )); + } - let year = expect_number(&args[0])? as i32; - let month = expect_number(&args[1])? as u32; - let day = expect_number(&args[2])? as u32; + let year = match &args[0] { + Value::Number(n) => *n as i32, + _ => { + return Err(RuntimeError::new( + format!( + "create_date expects a Number as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; + + let month = match &args[1] { + Value::Number(n) => *n as u32, + _ => { + return Err(RuntimeError::new( + format!( + "create_date expects a Number as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; + + let day = match &args[2] { + Value::Number(n) => *n as u32, + _ => { + return Err(RuntimeError::new( + format!( + "create_date expects a Number as third argument, got {}", + args[2].type_name() + ), + 0, + 0, + )); + } + }; if !(1..=12).contains(&month) { return Err(RuntimeError::new( @@ -184,10 +440,41 @@ pub fn native_create_date(args: Vec) -> Result { /// Adds days to a date pub fn native_add_days(args: Vec) -> Result { - check_arg_count("add_days", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("add_days expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } + + let date = match &args[0] { + Value::Date(d) => d.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "add_days expects a Date as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; - let date = expect_date(&args[0])?; - let days = expect_number(&args[1])? as i64; + let days = match &args[1] { + Value::Number(n) => *n as i64, + _ => { + return Err(RuntimeError::new( + format!( + "add_days expects a Number as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let new_date = date .checked_add_signed(chrono::Duration::days(days)) @@ -198,10 +485,41 @@ pub fn native_add_days(args: Vec) -> Result { /// Gets the difference in days between two dates pub fn native_days_between(args: Vec) -> Result { - check_arg_count("days_between", &args, 2)?; + if args.len() != 2 { + return Err(RuntimeError::new( + format!("days_between expects 2 arguments, got {}", args.len()), + 0, + 0, + )); + } - let date1 = expect_date(&args[0])?; - let date2 = expect_date(&args[1])?; + let date1 = match &args[0] { + Value::Date(d) => d.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "days_between expects a Date as first argument, got {}", + args[0].type_name() + ), + 0, + 0, + )); + } + }; + + let date2 = match &args[1] { + Value::Date(d) => d.clone(), + _ => { + return Err(RuntimeError::new( + format!( + "days_between expects a Date as second argument, got {}", + args[1].type_name() + ), + 0, + 0, + )); + } + }; let duration = date2.signed_duration_since(*date1); let days = duration.num_days(); @@ -211,7 +529,13 @@ pub fn native_days_between(args: Vec) -> Result { /// Simple test function that returns the current date as a string pub fn native_current_date(args: Vec) -> Result { - check_arg_count("current_date", &args, 0)?; + if !args.is_empty() { + return Err(RuntimeError::new( + format!("current_date expects 0 arguments, got {}", args.len()), + 0, + 0, + )); + } let today = Local::now().date_naive(); let formatted = today.format("%Y-%m-%d").to_string(); diff --git a/src/typechecker/mod.rs b/src/typechecker/mod.rs index a58ce1b3..eab04ab4 100644 --- a/src/typechecker/mod.rs +++ b/src/typechecker/mod.rs @@ -1427,14 +1427,29 @@ impl TypeChecker { } } Statement::WaitForRequestStatement { - server: _server, + server, request_name: _request_name, - timeout: _timeout, + timeout, line: _line, column: _column, } => { - // TODO: Add type checking for server expression - // For now, just accept any type + self.validate_server_operand(server, *_line, *_column); + + if let Some(timeout_expr) = timeout { + let timeout_type = self.infer_expression_type(timeout_expr); + if timeout_type != Type::Number + && timeout_type != Type::Unknown + && timeout_type != Type::Error + { + self.type_error( + "Timeout must be a number".to_string(), + Some(Type::Number), + Some(timeout_type), + *_line, + *_column, + ); + } + } } Statement::RespondStatement { request: _request, @@ -1500,20 +1515,18 @@ impl TypeChecker { self.validate_signal_handler_statement(signal_type, handler_name, *line, *column); } Statement::StopAcceptingConnectionsStatement { - server: _server, + server, line: _line, column: _column, } => { - // TODO: Add type checking for server expression - // For now, just accept any type + self.validate_server_operand(server, *_line, *_column); } Statement::CloseServerStatement { - server: _server, + server, line: _line, column: _column, } => { - // TODO: Add type checking for server expression - // For now, just accept any type + self.validate_server_operand(server, *_line, *_column); } // Test framework statements Statement::DescribeBlock { @@ -2974,6 +2987,23 @@ impl TypeChecker { } } + fn validate_server_operand(&mut self, server: &Expression, line: usize, column: usize) { + let server_type = self.infer_expression_type(server); + if server_type != Type::Custom("Server".to_string()) + && server_type != Type::Text + && server_type != Type::Unknown + && server_type != Type::Error + { + self.type_error( + "Expected a Server object or server name (text)".to_string(), + Some(Type::Custom("Server".to_string())), + Some(server_type), + line, + column, + ); + } + } + #[allow(clippy::only_used_in_recursion)] fn check_return_statements( &mut self, @@ -3380,6 +3410,145 @@ mod tests { ); } + #[test] + fn test_server_statement_type_checking() { + // Test case 1: Valid server variable (mocked as Custom("Server")) + let valid_server_var = Program { + statements: vec![Statement::WaitForRequestStatement { + server: Expression::Variable("myServer".to_string(), 1, 30), + request_name: "req".to_string(), + timeout: None, + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + // Manually inject the server symbol to simulate a ListenStatement having run + type_checker + .analyzer + .define_symbol(Symbol { + name: "myServer".to_string(), + kind: SymbolKind::Variable { mutable: false }, + symbol_type: Some(Type::Custom("Server".to_string())), + line: 0, + column: 0, + }) + .unwrap(); + + let result = type_checker.check_types(&valid_server_var); + assert!( + result.is_ok(), + "Expected valid server variable to pass type checking" + ); + + // Test case 2: Valid text server name + let valid_text_server = Program { + statements: vec![Statement::WaitForRequestStatement { + server: Expression::Literal(Literal::String("server_name".to_string()), 1, 30), + request_name: "req".to_string(), + timeout: None, + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + let result = type_checker.check_types(&valid_text_server); + assert!( + result.is_ok(), + "Expected valid text server name to pass type checking" + ); + + // Test case 3: Invalid server type (Number) + let invalid_server_type = Program { + statements: vec![Statement::WaitForRequestStatement { + server: Expression::Literal(Literal::Integer(123), 1, 30), + request_name: "req".to_string(), + timeout: None, + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + let result = type_checker.check_types(&invalid_server_type); + assert!( + result.is_err(), + "Expected type error for numeric server name" + ); + let errors = result.err().unwrap(); + assert!( + errors.iter().any(|e| e + .message + .contains("Expected a Server object or server name")), + "Error message should mention expected types" + ); + + // Test case 4: Valid timeout + let valid_timeout = Program { + statements: vec![Statement::WaitForRequestStatement { + server: Expression::Literal(Literal::String("server".to_string()), 1, 30), + request_name: "req".to_string(), + timeout: Some(Expression::Literal(Literal::Integer(5000), 1, 50)), + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + let result = type_checker.check_types(&valid_timeout); + assert!( + result.is_ok(), + "Expected valid timeout to pass type checking" + ); + + // Test case 5: Invalid timeout type + let invalid_timeout = Program { + statements: vec![Statement::WaitForRequestStatement { + server: Expression::Literal(Literal::String("server".to_string()), 1, 30), + request_name: "req".to_string(), + timeout: Some(Expression::Literal( + Literal::String("5s".to_string()), + 1, + 50, + )), + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + let result = type_checker.check_types(&invalid_timeout); + assert!( + result.is_err(), + "Expected type error for invalid timeout type" + ); + let errors = result.err().unwrap(); + assert!( + errors + .iter() + .any(|e| e.message.contains("Timeout must be a number")), + "Error message should mention timeout type requirement" + ); + + // Test case 6: Close server validation + let invalid_close = Program { + statements: vec![Statement::CloseServerStatement { + server: Expression::Literal(Literal::Integer(123), 1, 15), + line: 1, + column: 1, + }], + }; + + let mut type_checker = TypeChecker::new(); + let result = type_checker.check_types(&invalid_close); + assert!( + result.is_err(), + "Expected type error for closing numeric server" + ); + } + #[test] fn test_signal_handler_type_checking() { // Test case 1: Valid registration diff --git a/src/version.rs b/src/version.rs index dc863b21..77854472 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "26.2.5"; +pub const VERSION: &str = "26.1.56"; diff --git a/tests/time_stdlib_test.rs b/tests/time_stdlib_test.rs deleted file mode 100644 index 29e9846a..00000000 --- a/tests/time_stdlib_test.rs +++ /dev/null @@ -1,90 +0,0 @@ -use wfl::interpreter::value::Value; -use wfl::stdlib::time::native_create_time; - -#[test] -fn test_native_create_time_with_three_args() { - // Verify create_time with 3 args (hour, minute, second) works correctly - let args = vec![ - Value::Number(10.0), - Value::Number(30.0), - Value::Number(45.0), - ]; - let result = native_create_time(args); - assert!( - result.is_ok(), - "create_time should succeed with valid hour, minute, and second" - ); - - // Verify the returned value is a Time variant - let time_value = result.unwrap(); - assert!( - matches!(time_value, Value::Time(_)), - "create_time should return a Time value" - ); -} - -#[test] -fn test_native_create_time_with_two_args() { - // Verify create_time with 2 args (hour, minute) works correctly (seconds defaults to 0) - let args = vec![Value::Number(10.0), Value::Number(30.0)]; - let result = native_create_time(args); - assert!( - result.is_ok(), - "create_time should succeed with valid hour and minute (seconds defaults to 0)" - ); - - // Verify the returned value is a Time variant - let time_value = result.unwrap(); - assert!( - matches!(time_value, Value::Time(_)), - "create_time should return a Time value" - ); -} - -#[test] -fn test_native_create_time_wrong_arg_count() { - // Verify create_time with wrong number of args returns error - let result = native_create_time(vec![]); - assert!(result.is_err(), "create_time should fail with 0 arguments"); - - let result = native_create_time(vec![Value::Number(10.0)]); - assert!(result.is_err(), "create_time should fail with 1 argument"); - - let result = native_create_time(vec![ - Value::Number(10.0), - Value::Number(30.0), - Value::Number(45.0), - Value::Number(0.0), - ]); - assert!(result.is_err(), "create_time should fail with 4 arguments"); -} - -#[test] -fn test_native_create_time_invalid_values() { - // Verify create_time with invalid hour - let args = vec![ - Value::Number(25.0), // Invalid hour - Value::Number(30.0), - Value::Number(45.0), - ]; - let result = native_create_time(args); - assert!(result.is_err(), "create_time should fail with hour > 23"); - - // Verify create_time with invalid minute - let args = vec![ - Value::Number(10.0), - Value::Number(60.0), // Invalid minute - Value::Number(45.0), - ]; - let result = native_create_time(args); - assert!(result.is_err(), "create_time should fail with minute >= 60"); - - // Verify create_time with invalid second - let args = vec![ - Value::Number(10.0), - Value::Number(30.0), - Value::Number(60.0), // Invalid second - ]; - let result = native_create_time(args); - assert!(result.is_err(), "create_time should fail with second >= 60"); -} diff --git a/tests/value_equality.rs b/tests/value_equality.rs deleted file mode 100644 index 43f5c30f..00000000 --- a/tests/value_equality.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use wfl::interpreter::value::{ContainerInstanceValue, Value}; -use wfl::stdlib::list::native_contains; - -#[test] -fn test_list_equality() { - let list1 = Value::List(Rc::new(RefCell::new(vec![ - Value::Number(1.0), - Value::Number(2.0), - ]))); - let list2 = Value::List(Rc::new(RefCell::new(vec![ - Value::Number(1.0), - Value::Number(2.0), - ]))); - let list3 = Value::List(Rc::new(RefCell::new(vec![ - Value::Number(1.0), - Value::Number(3.0), - ]))); - - assert_eq!(list1, list2, "Lists with same content should be equal"); - assert_ne!( - list1, list3, - "Lists with different content should not be equal" - ); -} - -#[test] -fn test_object_equality() { - let mut map1 = HashMap::new(); - map1.insert("a".to_string(), Value::Number(1.0)); - let obj1 = Value::Object(Rc::new(RefCell::new(map1))); - - let mut map2 = HashMap::new(); - map2.insert("a".to_string(), Value::Number(1.0)); - let obj2 = Value::Object(Rc::new(RefCell::new(map2))); - - let mut map3 = HashMap::new(); - map3.insert("a".to_string(), Value::Number(2.0)); - let obj3 = Value::Object(Rc::new(RefCell::new(map3))); - - assert_eq!(obj1, obj2, "Objects with same content should be equal"); - assert_ne!( - obj1, obj3, - "Objects with different content should not be equal" - ); -} - -#[test] -fn test_nested_equality() { - // Nested lists - let inner1 = Value::List(Rc::new(RefCell::new(vec![Value::Number(1.0)]))); - let inner2 = Value::List(Rc::new(RefCell::new(vec![Value::Number(1.0)]))); - - let list1 = Value::List(Rc::new(RefCell::new(vec![inner1]))); - let list2 = Value::List(Rc::new(RefCell::new(vec![inner2]))); - - assert_eq!( - list1, list2, - "Nested lists with same content should be equal" - ); -} - -#[test] -fn test_native_contains() { - let list_val = Value::List(Rc::new(RefCell::new(vec![Value::Number(10.0)]))); - let args = vec![list_val.clone(), Value::Number(10.0)]; - let result = native_contains(args).unwrap(); - assert_eq!(result, Value::Bool(true)); - - let args2 = vec![list_val.clone(), Value::Number(20.0)]; - let result2 = native_contains(args2).unwrap(); - assert_eq!(result2, Value::Bool(false)); - - // Test with object in list - let mut map = HashMap::new(); - map.insert("k".to_string(), Value::Number(1.0)); - let obj = Value::Object(Rc::new(RefCell::new(map))); - - let list_obj = Value::List(Rc::new(RefCell::new(vec![obj.clone()]))); - - // Construct identical object - let mut map2 = HashMap::new(); - map2.insert("k".to_string(), Value::Number(1.0)); - let obj2 = Value::Object(Rc::new(RefCell::new(map2))); - - let args3 = vec![list_obj.clone(), obj2]; - let result3 = native_contains(args3).unwrap(); - assert_eq!( - result3, - Value::Bool(true), - "Should find structurally equal object" - ); -} - -#[test] -fn test_cyclic_list_equality() { - let list1_rc = Rc::new(RefCell::new(vec![])); - let list1 = Value::List(list1_rc.clone()); - list1_rc.borrow_mut().push(list1.clone()); - - let list2_rc = Rc::new(RefCell::new(vec![])); - let list2 = Value::List(list2_rc.clone()); - list2_rc.borrow_mut().push(list2.clone()); - - assert_eq!( - list1, list2, - "Cyclic lists should be equal and not stack overflow" - ); -} - -#[test] -fn test_cyclic_object_equality() { - let obj1_rc = Rc::new(RefCell::new(HashMap::new())); - let obj1 = Value::Object(obj1_rc.clone()); - obj1_rc - .borrow_mut() - .insert("self".to_string(), obj1.clone()); - - let obj2_rc = Rc::new(RefCell::new(HashMap::new())); - let obj2 = Value::Object(obj2_rc.clone()); - obj2_rc - .borrow_mut() - .insert("self".to_string(), obj2.clone()); - - assert_eq!( - obj1, obj2, - "Cyclic objects should be equal and not stack overflow" - ); -} - -#[test] -fn test_comparison_with_borrowed_value() { - let list1_rc = Rc::new(RefCell::new(vec![Value::Number(1.0)])); - let list1 = Value::List(list1_rc.clone()); - - let list2_rc = Rc::new(RefCell::new(vec![Value::Number(1.0)])); - let list2 = Value::List(list2_rc.clone()); - - // Mutably borrow list1 - let _borrow = list1_rc.borrow_mut(); - - // Should return false (or not panic) when comparing a borrowed value - // because we can't inspect its contents safely - assert_ne!( - list1, list2, - "Comparison with borrowed value should not be equal" - ); -} - -#[test] -fn test_container_parent_comparison() { - // Create Parent Instance 1 - let parent1 = Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Parent".to_string(), - properties: HashMap::from([("p".to_string(), Value::Number(1.0))]), - parent: None, - line: 0, - column: 0, - })); - - // Create Child 1 with Parent 1 - let child1 = Value::ContainerInstance(Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Child".to_string(), - properties: HashMap::new(), - parent: Some(parent1), - line: 0, - column: 0, - }))); - - // Create Parent Instance 2 (Identical to Parent 1) - let parent2 = Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Parent".to_string(), - properties: HashMap::from([("p".to_string(), Value::Number(1.0))]), - parent: None, - line: 0, - column: 0, - })); - - // Create Child 2 with Parent 2 - let child2 = Value::ContainerInstance(Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Child".to_string(), - properties: HashMap::new(), - parent: Some(parent2), - line: 0, - column: 0, - }))); - - // Create Parent Instance 3 (Different Property) - let parent3 = Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Parent".to_string(), - properties: HashMap::from([("p".to_string(), Value::Number(2.0))]), - parent: None, - line: 0, - column: 0, - })); - - // Create Child 3 with Parent 3 - let child3 = Value::ContainerInstance(Rc::new(RefCell::new(ContainerInstanceValue { - container_type: "Child".to_string(), - properties: HashMap::new(), - parent: Some(parent3), - line: 0, - column: 0, - }))); - - assert_eq!( - child1, child2, - "Containers with identical parents should be equal" - ); - assert_ne!( - child1, child3, - "Containers with different parents should not be equal" - ); -} diff --git a/tests/wflhash_hardened_security_test.rs b/tests/wflhash_hardened_security_test.rs index d4c0202b..7702b3b4 100644 --- a/tests/wflhash_hardened_security_test.rs +++ b/tests/wflhash_hardened_security_test.rs @@ -111,8 +111,8 @@ mod wflhash_hardened_security_tests { assert!(result.is_err(), "Should fail with wrong arg count"); if let Err(e) = result { assert_eq!( - e.message, "wflhash256 expects 1 argument, got 0", - "Error should be standard" + e.message, "Invalid argument count", + "Error should be generic" ); } @@ -121,8 +121,8 @@ mod wflhash_hardened_security_tests { assert!(result.is_err(), "Should fail with wrong arg type"); if let Err(e) = result { assert_eq!( - e.message, "Expected text, got Number", - "Error should be standard" + e.message, "Invalid argument type", + "Error should be generic" ); } @@ -131,8 +131,8 @@ mod wflhash_hardened_security_tests { assert!(result.is_err(), "MAC should fail with wrong arg count"); if let Err(e) = result { assert_eq!( - e.message, "wflmac256 expects 2 arguments, got 1", - "Error should be standard" + e.message, "Invalid argument count", + "Error should be generic" ); } } diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 91d864f1..186e0de7 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "vscode-wfl", "displayName": "WebFirst Language", "description": "WebFirst Language (WFL) support for VS Code", - "version": "26.2.5", + "version": "26.1.56", "publisher": "wfl", "license": "MIT", "engines": { diff --git a/wix.toml b/wix.toml index 5f277439..f138fd23 100644 --- a/wix.toml +++ b/wix.toml @@ -3,7 +3,7 @@ [package] name = "WFL" manufacturer = "Logbie LLC" -version = "26.2.5.0" # Updated by bump_version.py +version = "26.1.56.0" # Updated by bump_version.py description = "WebFirst Language" license = "LICENSE"