diff --git a/examples/stdlib_demo.woke b/examples/stdlib_demo.woke new file mode 100644 index 0000000..c85fd8c --- /dev/null +++ b/examples/stdlib_demo.woke @@ -0,0 +1,91 @@ +// Example: WokeLang Standard Library Demo +// Demonstrates usage of std.math, std.time, std.json, std.io, and std.net + +thanks to { + "WokeLang Standard Library" -> "For comprehensive utilities"; +} + +to main() { + hello "Standard Library Demo"; + + print("=== Math Functions ==="); + // Math doesn't require capabilities + print("abs(-42) = " + toString(std.math.abs(-42))); + print("sqrt(16) = " + toString(std.math.sqrt(16))); + print("pow(2, 8) = " + toString(std.math.pow(2, 8))); + print("floor(3.7) = " + toString(std.math.floor(3.7))); + print("ceil(3.2) = " + toString(std.math.ceil(3.2))); + print("min(10, 5) = " + toString(std.math.min(10, 5))); + print("max(10, 5) = " + toString(std.math.max(10, 5))); + print("PI = " + toString(std.math.pi())); + print("E = " + toString(std.math.e())); + + print(""); + print("=== Time Functions ==="); + remember now_ms = std.time.now(); + print("Current time (ms): " + toString(now_ms)); + + remember formatted = std.time.format(now_ms, "%Y-%m-%d %H:%M:%S"); + print("Formatted: " + formatted); + + // Elapsed timer example + std.time.elapsed("start", "demo_timer"); + // Do some work... + std.time.sleep(100); + remember elapsed_ms = std.time.elapsed("stop", "demo_timer"); + print("Elapsed: " + toString(elapsed_ms) + "ms"); + + print(""); + print("=== JSON Functions ==="); + // Parse JSON + remember json_str = "{\"name\": \"WokeLang\", \"version\": 1, \"features\": [\"consent\", \"safety\"]}"; + remember parsed = std.json.parse(json_str); + print("Parsed JSON: " + toString(parsed)); + + // Get nested value + remember name = std.json.get(parsed, "name"); + print("Name: " + toString(name)); + + // Stringify + remember back_to_string = std.json.stringify(parsed); + print("Stringified: " + back_to_string); + + print(""); + print("=== File I/O (requires consent) ==="); + // These operations require file:read and file:write capabilities + // In a real program, the user would be prompted for consent + + only if okay { + superpower file:write; + + remember path = "/tmp/wokelang_demo.txt"; + std.io.writeFile(path, "Hello from WokeLang!"); + print("Wrote to: " + path); + + remember content = std.io.readFile(path); + print("Read back: " + content); + + remember exists = std.io.exists(path); + print("File exists: " + toString(exists)); + + std.io.delete(path); + print("File deleted"); + } + + print(""); + print("=== Network (requires consent) ==="); + // Network operations require network:* capability + // Note: HTTPS requires TLS library + + only if okay { + superpower network:*; + + // HTTP GET (would work with a real HTTP server) + // remember response = std.net.httpGet("http://example.com"); + // print("Response: " + response); + + print("Network functions available but require actual server"); + } + + goodbye "Demo complete!"; +} diff --git a/src/stdlib/io.rs b/src/stdlib/io.rs new file mode 100644 index 0000000..860eb9a --- /dev/null +++ b/src/stdlib/io.rs @@ -0,0 +1,287 @@ +//! WokeLang Standard Library - I/O Module +//! +//! File I/O operations that require explicit consent through capabilities. + +use crate::interpreter::Value; +use crate::security::{Capability, CapabilityRegistry}; +use super::{check_arity, check_arity_range, expect_string, StdlibError}; +use std::fs; +use std::io::{self, BufRead, Write}; +use std::path::PathBuf; + +/// Helper to require file read capability +fn require_read(path: &str, caps: &mut CapabilityRegistry) -> Result<(), StdlibError> { + let cap = Capability::FileRead(Some(PathBuf::from(path))); + if caps.request("stdlib", &cap).is_err() { + Err(StdlibError::PermissionDenied(format!( + "File read access denied: {}", + path + ))) + } else { + Ok(()) + } +} + +/// Helper to require file write capability +fn require_write(path: &str, caps: &mut CapabilityRegistry) -> Result<(), StdlibError> { + let cap = Capability::FileWrite(Some(PathBuf::from(path))); + if caps.request("stdlib", &cap).is_err() { + Err(StdlibError::PermissionDenied(format!( + "File write access denied: {}", + path + ))) + } else { + Ok(()) + } +} + +/// Read entire file contents as a string +pub fn read_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let path = expect_string(&args[0], "path")?; + + require_read(&path, caps)?; + + match fs::read_to_string(&path) { + Ok(contents) => Ok(Value::String(contents)), + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// Write string contents to a file +pub fn write_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let path = expect_string(&args[0], "path")?; + let contents = expect_string(&args[1], "contents")?; + + require_write(&path, caps)?; + + match fs::write(&path, &contents) { + Ok(()) => Ok(Value::Bool(true)), + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// Append string contents to a file +pub fn append_file(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let path = expect_string(&args[0], "path")?; + let contents = expect_string(&args[1], "contents")?; + + require_write(&path, caps)?; + + use std::fs::OpenOptions; + match OpenOptions::new().create(true).append(true).open(&path) { + Ok(mut file) => match file.write_all(contents.as_bytes()) { + Ok(()) => Ok(Value::Bool(true)), + Err(e) => Err(StdlibError::IoError(e.to_string())), + }, + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// Check if a file or directory exists +pub fn exists(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let path = expect_string(&args[0], "path")?; + + // exists only needs read capability to check + require_read(&path, caps)?; + + Ok(Value::Bool(std::path::Path::new(&path).exists())) +} + +/// Delete a file +pub fn delete(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let path = expect_string(&args[0], "path")?; + + require_write(&path, caps)?; + + match fs::remove_file(&path) { + Ok(()) => Ok(Value::Bool(true)), + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// List directory contents +pub fn list_dir(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let path = expect_string(&args[0], "path")?; + + require_read(&path, caps)?; + + match fs::read_dir(&path) { + Ok(entries) => { + let files: Vec = entries + .filter_map(|e| e.ok()) + .map(|e| Value::String(e.file_name().to_string_lossy().to_string())) + .collect(); + Ok(Value::Array(files)) + } + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// Create a directory (and parents if needed) +pub fn create_dir(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let path = expect_string(&args[0], "path")?; + + require_write(&path, caps)?; + + match fs::create_dir_all(&path) { + Ok(()) => Ok(Value::Bool(true)), + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +/// Read a line from stdin (interactive) +pub fn read_line(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 0, 1)?; + + // Print optional prompt + if let Some(prompt) = args.first() { + let prompt_str = expect_string(prompt, "prompt")?; + print!("{}", prompt_str); + io::stdout().flush().ok(); + } + + let stdin = io::stdin(); + let mut line = String::new(); + match stdin.lock().read_line(&mut line) { + Ok(_) => Ok(Value::String(line.trim_end_matches('\n').to_string())), + Err(e) => Err(StdlibError::IoError(e.to_string())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + fn temp_file(name: &str) -> String { + env::temp_dir() + .join(format!("wokelang_test_{}", name)) + .to_string_lossy() + .to_string() + } + + #[test] + fn test_write_and_read() { + let mut caps = test_caps(); + let path = temp_file("io_test.txt"); + + // Write + let write_result = write_file( + &[Value::String(path.clone()), Value::String("Hello, WokeLang!".to_string())], + &mut caps, + ); + assert!(write_result.is_ok()); + + // Read + let read_result = read_file(&[Value::String(path.clone())], &mut caps); + assert_eq!( + read_result.unwrap(), + Value::String("Hello, WokeLang!".to_string()) + ); + + // Cleanup + let _ = fs::remove_file(&path); + } + + #[test] + fn test_exists() { + let mut caps = test_caps(); + let path = temp_file("exists_test.txt"); + + // Should not exist yet + let result = exists(&[Value::String(path.clone())], &mut caps); + assert_eq!(result.unwrap(), Value::Bool(false)); + + // Create file + fs::write(&path, "test").unwrap(); + + // Should exist now + let result = exists(&[Value::String(path.clone())], &mut caps); + assert_eq!(result.unwrap(), Value::Bool(true)); + + // Cleanup + let _ = fs::remove_file(&path); + } + + #[test] + fn test_append() { + let mut caps = test_caps(); + let path = temp_file("append_test.txt"); + + // Write initial content + write_file( + &[Value::String(path.clone()), Value::String("Hello".to_string())], + &mut caps, + ) + .unwrap(); + + // Append + append_file( + &[Value::String(path.clone()), Value::String(", World!".to_string())], + &mut caps, + ) + .unwrap(); + + // Read and verify + let result = read_file(&[Value::String(path.clone())], &mut caps); + assert_eq!( + result.unwrap(), + Value::String("Hello, World!".to_string()) + ); + + // Cleanup + let _ = fs::remove_file(&path); + } + + #[test] + fn test_delete() { + let mut caps = test_caps(); + let path = temp_file("delete_test.txt"); + + // Create file + fs::write(&path, "test").unwrap(); + assert!(std::path::Path::new(&path).exists()); + + // Delete + let result = delete(&[Value::String(path.clone())], &mut caps); + assert!(result.is_ok()); + assert!(!std::path::Path::new(&path).exists()); + } + + #[test] + fn test_create_dir_and_list() { + let mut caps = test_caps(); + let dir_path = temp_file("test_dir"); + + // Create directory + create_dir(&[Value::String(dir_path.clone())], &mut caps).unwrap(); + assert!(std::path::Path::new(&dir_path).is_dir()); + + // Create a file in the directory + let file_path = format!("{}/test.txt", dir_path); + fs::write(&file_path, "test").unwrap(); + + // List directory + let result = list_dir(&[Value::String(dir_path.clone())], &mut caps); + match result.unwrap() { + Value::Array(files) => { + assert!(files.contains(&Value::String("test.txt".to_string()))); + } + _ => panic!("Expected array"), + } + + // Cleanup + let _ = fs::remove_dir_all(&dir_path); + } +} diff --git a/src/stdlib/json.rs b/src/stdlib/json.rs new file mode 100644 index 0000000..fe2516e --- /dev/null +++ b/src/stdlib/json.rs @@ -0,0 +1,509 @@ +//! WokeLang Standard Library - JSON Module +//! +//! JSON parsing and generation functions. + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use super::{check_arity, expect_string, StdlibError}; +use std::collections::HashMap; + +/// Simple JSON tokenizer +#[derive(Debug, Clone, PartialEq)] +enum JsonToken { + LBrace, + RBrace, + LBracket, + RBracket, + Colon, + Comma, + String(String), + Number(f64), + True, + False, + Null, +} + +/// Tokenize JSON string +fn tokenize(input: &str) -> Result, StdlibError> { + let mut tokens = Vec::new(); + let mut chars = input.chars().peekable(); + + while let Some(&c) = chars.peek() { + match c { + ' ' | '\t' | '\n' | '\r' => { + chars.next(); + } + '{' => { + chars.next(); + tokens.push(JsonToken::LBrace); + } + '}' => { + chars.next(); + tokens.push(JsonToken::RBrace); + } + '[' => { + chars.next(); + tokens.push(JsonToken::LBracket); + } + ']' => { + chars.next(); + tokens.push(JsonToken::RBracket); + } + ':' => { + chars.next(); + tokens.push(JsonToken::Colon); + } + ',' => { + chars.next(); + tokens.push(JsonToken::Comma); + } + '"' => { + chars.next(); + let mut s = String::new(); + while let Some(&c) = chars.peek() { + if c == '"' { + chars.next(); + break; + } else if c == '\\' { + chars.next(); + match chars.next() { + Some('n') => s.push('\n'), + Some('t') => s.push('\t'), + Some('r') => s.push('\r'), + Some('"') => s.push('"'), + Some('\\') => s.push('\\'), + Some('/') => s.push('/'), + Some(c) => s.push(c), + None => { + return Err(StdlibError::ParseError( + "Unterminated escape sequence".to_string(), + )) + } + } + } else { + s.push(c); + chars.next(); + } + } + tokens.push(JsonToken::String(s)); + } + '-' | '0'..='9' => { + let mut num_str = String::new(); + while let Some(&c) = chars.peek() { + if c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E' || c.is_ascii_digit() + { + num_str.push(c); + chars.next(); + } else { + break; + } + } + let num: f64 = num_str.parse().map_err(|_| { + StdlibError::ParseError(format!("Invalid number: {}", num_str)) + })?; + tokens.push(JsonToken::Number(num)); + } + 't' => { + for expected in ['t', 'r', 'u', 'e'] { + if chars.next() != Some(expected) { + return Err(StdlibError::ParseError("Expected 'true'".to_string())); + } + } + tokens.push(JsonToken::True); + } + 'f' => { + for expected in ['f', 'a', 'l', 's', 'e'] { + if chars.next() != Some(expected) { + return Err(StdlibError::ParseError("Expected 'false'".to_string())); + } + } + tokens.push(JsonToken::False); + } + 'n' => { + for expected in ['n', 'u', 'l', 'l'] { + if chars.next() != Some(expected) { + return Err(StdlibError::ParseError("Expected 'null'".to_string())); + } + } + tokens.push(JsonToken::Null); + } + _ => { + return Err(StdlibError::ParseError(format!( + "Unexpected character: {}", + c + ))) + } + } + } + + Ok(tokens) +} + +/// Parse JSON tokens into Value +fn parse_value(tokens: &[JsonToken], pos: &mut usize) -> Result { + if *pos >= tokens.len() { + return Err(StdlibError::ParseError("Unexpected end of input".to_string())); + } + + match &tokens[*pos] { + JsonToken::LBrace => parse_object(tokens, pos), + JsonToken::LBracket => parse_array(tokens, pos), + JsonToken::String(s) => { + *pos += 1; + Ok(Value::String(s.clone())) + } + JsonToken::Number(n) => { + *pos += 1; + if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 { + Ok(Value::Int(*n as i64)) + } else { + Ok(Value::Float(*n)) + } + } + JsonToken::True => { + *pos += 1; + Ok(Value::Bool(true)) + } + JsonToken::False => { + *pos += 1; + Ok(Value::Bool(false)) + } + JsonToken::Null => { + *pos += 1; + Ok(Value::Unit) + } + _ => Err(StdlibError::ParseError(format!( + "Unexpected token: {:?}", + tokens[*pos] + ))), + } +} + +/// Parse JSON object +fn parse_object(tokens: &[JsonToken], pos: &mut usize) -> Result { + *pos += 1; // consume '{' + + let mut map = HashMap::new(); + + if *pos < tokens.len() && tokens[*pos] == JsonToken::RBrace { + *pos += 1; + return Ok(Value::Record(map)); + } + + loop { + // Expect string key + let key = match &tokens[*pos] { + JsonToken::String(s) => { + *pos += 1; + s.clone() + } + _ => return Err(StdlibError::ParseError("Expected string key".to_string())), + }; + + // Expect colon + if *pos >= tokens.len() || tokens[*pos] != JsonToken::Colon { + return Err(StdlibError::ParseError("Expected ':'".to_string())); + } + *pos += 1; + + // Parse value + let value = parse_value(tokens, pos)?; + map.insert(key, value); + + // Check for comma or end + if *pos >= tokens.len() { + return Err(StdlibError::ParseError("Unexpected end of object".to_string())); + } + + match &tokens[*pos] { + JsonToken::Comma => { + *pos += 1; + } + JsonToken::RBrace => { + *pos += 1; + break; + } + _ => return Err(StdlibError::ParseError("Expected ',' or '}'".to_string())), + } + } + + Ok(Value::Record(map)) +} + +/// Parse JSON array +fn parse_array(tokens: &[JsonToken], pos: &mut usize) -> Result { + *pos += 1; // consume '[' + + let mut items = Vec::new(); + + if *pos < tokens.len() && tokens[*pos] == JsonToken::RBracket { + *pos += 1; + return Ok(Value::Array(items)); + } + + loop { + let value = parse_value(tokens, pos)?; + items.push(value); + + if *pos >= tokens.len() { + return Err(StdlibError::ParseError("Unexpected end of array".to_string())); + } + + match &tokens[*pos] { + JsonToken::Comma => { + *pos += 1; + } + JsonToken::RBracket => { + *pos += 1; + break; + } + _ => return Err(StdlibError::ParseError("Expected ',' or ']'".to_string())), + } + } + + Ok(Value::Array(items)) +} + +/// Convert Value to JSON string +fn stringify_value(value: &Value) -> String { + match value { + Value::Unit => "null".to_string(), + Value::Bool(b) => b.to_string(), + Value::Int(n) => n.to_string(), + Value::Float(n) => { + if n.is_finite() { + n.to_string() + } else { + "null".to_string() + } + } + Value::String(s) => { + let escaped = s + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t"); + format!("\"{}\"", escaped) + } + Value::Array(items) => { + let items_str: Vec = items.iter().map(stringify_value).collect(); + format!("[{}]", items_str.join(",")) + } + Value::Record(map) => { + let pairs: Vec = map + .iter() + .map(|(k, v)| format!("\"{}\":{}", k, stringify_value(v))) + .collect(); + format!("{{{}}}", pairs.join(",")) + } + Value::Okay(inner) => stringify_value(inner), + Value::Oops(msg) => format!("{{\"error\":\"{}\"}}", msg), + } +} + +/// Parse JSON string into WokeLang value +pub fn parse(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let json_str = expect_string(&args[0], "json")?; + + let tokens = tokenize(&json_str)?; + if tokens.is_empty() { + return Err(StdlibError::ParseError("Empty JSON".to_string())); + } + + let mut pos = 0; + let value = parse_value(&tokens, &mut pos)?; + + if pos < tokens.len() { + return Err(StdlibError::ParseError("Trailing content after JSON".to_string())); + } + + Ok(value) +} + +/// Convert WokeLang value to JSON string +pub fn stringify(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + Ok(Value::String(stringify_value(&args[0]))) +} + +/// Get a value from a JSON object by key path +pub fn get(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let path = expect_string(&args[1], "path")?; + + let mut current = args[0].clone(); + + for key in path.split('.') { + match ¤t { + Value::Record(map) => { + current = map + .get(key) + .cloned() + .ok_or_else(|| StdlibError::RuntimeError(format!("Key not found: {}", key)))?; + } + Value::Array(items) => { + let idx: usize = key.parse().map_err(|_| { + StdlibError::RuntimeError(format!("Invalid array index: {}", key)) + })?; + current = items + .get(idx) + .cloned() + .ok_or_else(|| StdlibError::RuntimeError(format!("Index out of bounds: {}", idx)))?; + } + _ => { + return Err(StdlibError::RuntimeError(format!( + "Cannot access key '{}' on non-object/array", + key + ))) + } + } + } + + Ok(current) +} + +/// Set a value in a JSON object by key +pub fn set(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 3)?; + let key = expect_string(&args[1], "key")?; + + match &args[0] { + Value::Record(map) => { + let mut new_map = map.clone(); + new_map.insert(key, args[2].clone()); + Ok(Value::Record(new_map)) + } + _ => Err(StdlibError::TypeError { + expected: "Record".to_string(), + got: format!("{:?}", args[0]), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + #[test] + fn test_parse_primitives() { + let mut caps = test_caps(); + + assert_eq!( + parse(&[Value::String("42".to_string())], &mut caps).unwrap(), + Value::Int(42) + ); + assert_eq!( + parse(&[Value::String("3.14".to_string())], &mut caps).unwrap(), + Value::Float(3.14) + ); + assert_eq!( + parse(&[Value::String("true".to_string())], &mut caps).unwrap(), + Value::Bool(true) + ); + assert_eq!( + parse(&[Value::String("false".to_string())], &mut caps).unwrap(), + Value::Bool(false) + ); + assert_eq!( + parse(&[Value::String("null".to_string())], &mut caps).unwrap(), + Value::Unit + ); + assert_eq!( + parse(&[Value::String("\"hello\"".to_string())], &mut caps).unwrap(), + Value::String("hello".to_string()) + ); + } + + #[test] + fn test_parse_array() { + let mut caps = test_caps(); + + let result = parse(&[Value::String("[1, 2, 3]".to_string())], &mut caps).unwrap(); + assert_eq!( + result, + Value::Array(vec![ + Value::Int(1), + Value::Int(2), + Value::Int(3) + ]) + ); + } + + #[test] + fn test_parse_object() { + let mut caps = test_caps(); + + let result = parse( + &[Value::String("{\"name\": \"WokeLang\", \"version\": 1}".to_string())], + &mut caps, + ) + .unwrap(); + + match result { + Value::Record(map) => { + assert_eq!(map.get("name"), Some(&Value::String("WokeLang".to_string()))); + assert_eq!(map.get("version"), Some(&Value::Int(1))); + } + _ => panic!("Expected record"), + } + } + + #[test] + fn test_stringify() { + let mut caps = test_caps(); + + assert_eq!( + stringify(&[Value::Int(42)], &mut caps).unwrap(), + Value::String("42".to_string()) + ); + assert_eq!( + stringify(&[Value::String("hello".to_string())], &mut caps).unwrap(), + Value::String("\"hello\"".to_string()) + ); + assert_eq!( + stringify(&[Value::Bool(true)], &mut caps).unwrap(), + Value::String("true".to_string()) + ); + } + + #[test] + fn test_get() { + let mut caps = test_caps(); + + let json = parse( + &[Value::String("{\"user\": {\"name\": \"Alice\"}}".to_string())], + &mut caps, + ) + .unwrap(); + + let result = get(&[json, Value::String("user.name".to_string())], &mut caps).unwrap(); + assert_eq!(result, Value::String("Alice".to_string())); + } + + #[test] + fn test_set() { + let mut caps = test_caps(); + + let json = parse(&[Value::String("{\"x\": 1}".to_string())], &mut caps).unwrap(); + + let result = set( + &[json, Value::String("y".to_string()), Value::Int(2)], + &mut caps, + ) + .unwrap(); + + match result { + Value::Record(map) => { + assert_eq!(map.get("x"), Some(&Value::Int(1))); + assert_eq!(map.get("y"), Some(&Value::Int(2))); + } + _ => panic!("Expected record"), + } + } +} diff --git a/src/stdlib/math.rs b/src/stdlib/math.rs new file mode 100644 index 0000000..83485fe --- /dev/null +++ b/src/stdlib/math.rs @@ -0,0 +1,229 @@ +//! WokeLang Standard Library - Math Module +//! +//! Mathematical functions that don't require any special capabilities. + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use super::{check_arity, check_arity_range, expect_float, StdlibError}; +use std::f64::consts::{E, PI}; + +/// Absolute value +pub fn abs(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + match &args[0] { + Value::Int(n) => Ok(Value::Int(n.abs())), + Value::Float(n) => Ok(Value::Float(n.abs())), + other => Err(StdlibError::TypeError { + expected: "Int or Float".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Square root +pub fn sqrt(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + if n < 0.0 { + Err(StdlibError::RuntimeError( + "Cannot take square root of negative number".to_string(), + )) + } else { + Ok(Value::Float(n.sqrt())) + } +} + +/// Power function +pub fn pow(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let base = expect_float(&args[0], "base")?; + let exp = expect_float(&args[1], "exponent")?; + Ok(Value::Float(base.powf(exp))) +} + +/// Sine function (radians) +pub fn sin(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Float(n.sin())) +} + +/// Cosine function (radians) +pub fn cos(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Float(n.cos())) +} + +/// Tangent function (radians) +pub fn tan(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Float(n.tan())) +} + +/// Floor function +pub fn floor(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Int(n.floor() as i64)) +} + +/// Ceiling function +pub fn ceil(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Int(n.ceil() as i64)) +} + +/// Round function +pub fn round(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let n = expect_float(&args[0], "n")?; + Ok(Value::Int(n.round() as i64)) +} + +/// Minimum of two values +pub fn min(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(*a.min(b))), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a.min(*b))), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float((*a as f64).min(*b))), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a.min(*b as f64))), + _ => Err(StdlibError::TypeError { + expected: "Int or Float".to_string(), + got: "non-numeric".to_string(), + }), + } +} + +/// Maximum of two values +pub fn max(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(*a.max(b))), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a.max(*b))), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float((*a as f64).max(*b))), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a.max(*b as f64))), + _ => Err(StdlibError::TypeError { + expected: "Int or Float".to_string(), + got: "non-numeric".to_string(), + }), + } +} + +/// Random number between 0 and 1 (or between min and max if provided) +pub fn random(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 0, 2)?; + + // Simple pseudo-random using system time + use std::time::{SystemTime, UNIX_EPOCH}; + let seed = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + + // Simple LCG + let random_val = ((seed.wrapping_mul(1103515245).wrapping_add(12345)) % (1 << 31)) as f64 / (1u64 << 31) as f64; + + match args.len() { + 0 => Ok(Value::Float(random_val)), + 2 => { + let min = expect_float(&args[0], "min")?; + let max = expect_float(&args[1], "max")?; + Ok(Value::Float(min + random_val * (max - min))) + } + _ => Err(StdlibError::ArityError { expected: 0, got: args.len() }), + } +} + +/// Pi constant +pub fn pi(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 0)?; + Ok(Value::Float(PI)) +} + +/// E constant (Euler's number) +pub fn e(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 0)?; + Ok(Value::Float(E)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + #[test] + fn test_abs() { + let mut caps = test_caps(); + assert_eq!( + abs(&[Value::Int(-5)], &mut caps).unwrap(), + Value::Int(5) + ); + assert_eq!( + abs(&[Value::Float(-3.14)], &mut caps).unwrap(), + Value::Float(3.14) + ); + } + + #[test] + fn test_sqrt() { + let mut caps = test_caps(); + assert_eq!( + sqrt(&[Value::Int(16)], &mut caps).unwrap(), + Value::Float(4.0) + ); + assert!(sqrt(&[Value::Int(-1)], &mut caps).is_err()); + } + + #[test] + fn test_pow() { + let mut caps = test_caps(); + assert_eq!( + pow(&[Value::Int(2), Value::Int(3)], &mut caps).unwrap(), + Value::Float(8.0) + ); + } + + #[test] + fn test_floor_ceil_round() { + let mut caps = test_caps(); + assert_eq!( + floor(&[Value::Float(3.7)], &mut caps).unwrap(), + Value::Int(3) + ); + assert_eq!( + ceil(&[Value::Float(3.2)], &mut caps).unwrap(), + Value::Int(4) + ); + assert_eq!( + round(&[Value::Float(3.5)], &mut caps).unwrap(), + Value::Int(4) + ); + } + + #[test] + fn test_min_max() { + let mut caps = test_caps(); + assert_eq!( + min(&[Value::Int(3), Value::Int(7)], &mut caps).unwrap(), + Value::Int(3) + ); + assert_eq!( + max(&[Value::Int(3), Value::Int(7)], &mut caps).unwrap(), + Value::Int(7) + ); + } + + #[test] + fn test_constants() { + let mut caps = test_caps(); + assert_eq!(pi(&[], &mut caps).unwrap(), Value::Float(PI)); + assert_eq!(e(&[], &mut caps).unwrap(), Value::Float(E)); + } +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs new file mode 100644 index 0000000..b43ca92 --- /dev/null +++ b/src/stdlib/mod.rs @@ -0,0 +1,255 @@ +//! WokeLang Standard Library +//! +//! This module provides the standard library for WokeLang, offering +//! common functionality with consent-aware operations. + +pub mod io; +pub mod json; +pub mod math; +pub mod net; +pub mod time; + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use std::collections::HashMap; + +/// Standard library function signature +pub type StdlibFn = fn(&[Value], &mut CapabilityRegistry) -> Result; + +/// Error type for standard library operations +#[derive(Debug, Clone)] +pub enum StdlibError { + /// Wrong number of arguments + ArityError { expected: usize, got: usize }, + /// Wrong argument type + TypeError { expected: String, got: String }, + /// Capability not granted + PermissionDenied(String), + /// I/O error + IoError(String), + /// Network error + NetworkError(String), + /// Parse error + ParseError(String), + /// Other runtime error + RuntimeError(String), +} + +impl std::fmt::Display for StdlibError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StdlibError::ArityError { expected, got } => { + write!(f, "Expected {} arguments, got {}", expected, got) + } + StdlibError::TypeError { expected, got } => { + write!(f, "Expected {}, got {}", expected, got) + } + StdlibError::PermissionDenied(cap) => { + write!(f, "Permission denied: {}", cap) + } + StdlibError::IoError(msg) => write!(f, "I/O error: {}", msg), + StdlibError::NetworkError(msg) => write!(f, "Network error: {}", msg), + StdlibError::ParseError(msg) => write!(f, "Parse error: {}", msg), + StdlibError::RuntimeError(msg) => write!(f, "Runtime error: {}", msg), + } + } +} + +impl std::error::Error for StdlibError {} + +/// The standard library registry +pub struct StdlibRegistry { + functions: HashMap, +} + +impl StdlibRegistry { + pub fn new() -> Self { + let mut registry = Self { + functions: HashMap::new(), + }; + registry.register_all(); + registry + } + + /// Register all standard library functions + fn register_all(&mut self) { + // Math functions + self.register("std.math.abs", math::abs); + self.register("std.math.sqrt", math::sqrt); + self.register("std.math.pow", math::pow); + self.register("std.math.sin", math::sin); + self.register("std.math.cos", math::cos); + self.register("std.math.tan", math::tan); + self.register("std.math.floor", math::floor); + self.register("std.math.ceil", math::ceil); + self.register("std.math.round", math::round); + self.register("std.math.min", math::min); + self.register("std.math.max", math::max); + self.register("std.math.random", math::random); + self.register("std.math.pi", math::pi); + self.register("std.math.e", math::e); + + // I/O functions (require consent) + self.register("std.io.readFile", io::read_file); + self.register("std.io.writeFile", io::write_file); + self.register("std.io.appendFile", io::append_file); + self.register("std.io.exists", io::exists); + self.register("std.io.delete", io::delete); + self.register("std.io.listDir", io::list_dir); + self.register("std.io.createDir", io::create_dir); + self.register("std.io.readLine", io::read_line); + + // JSON functions + self.register("std.json.parse", json::parse); + self.register("std.json.stringify", json::stringify); + self.register("std.json.get", json::get); + self.register("std.json.set", json::set); + + // Time functions + self.register("std.time.now", time::now); + self.register("std.time.format", time::format); + self.register("std.time.parse", time::parse); + self.register("std.time.sleep", time::sleep); + self.register("std.time.timestamp", time::timestamp); + self.register("std.time.elapsed", time::elapsed); + + // Network functions (require consent) + self.register("std.net.httpGet", net::http_get); + self.register("std.net.httpPost", net::http_post); + self.register("std.net.download", net::download); + } + + /// Register a function + fn register(&mut self, name: &str, func: StdlibFn) { + self.functions.insert(name.to_string(), func); + } + + /// Get a function by name + pub fn get(&self, name: &str) -> Option<&StdlibFn> { + self.functions.get(name) + } + + /// Check if a function exists + pub fn has(&self, name: &str) -> bool { + self.functions.contains_key(name) + } + + /// List all available functions + pub fn list(&self) -> Vec<&str> { + self.functions.keys().map(|s| s.as_str()).collect() + } + + /// Call a standard library function + pub fn call( + &self, + name: &str, + args: &[Value], + capabilities: &mut CapabilityRegistry, + ) -> Result { + if let Some(func) = self.functions.get(name) { + func(args, capabilities) + } else { + Err(StdlibError::RuntimeError(format!( + "Unknown function: {}", + name + ))) + } + } +} + +impl Default for StdlibRegistry { + fn default() -> Self { + Self::new() + } +} + +/// Helper to check argument count +pub fn check_arity(args: &[Value], expected: usize) -> Result<(), StdlibError> { + if args.len() != expected { + Err(StdlibError::ArityError { + expected, + got: args.len(), + }) + } else { + Ok(()) + } +} + +/// Helper to check argument count range +pub fn check_arity_range(args: &[Value], min: usize, max: usize) -> Result<(), StdlibError> { + if args.len() < min || args.len() > max { + Err(StdlibError::ArityError { + expected: min, + got: args.len(), + }) + } else { + Ok(()) + } +} + +/// Helper to extract a string argument +pub fn expect_string(value: &Value, _arg_name: &str) -> Result { + match value { + Value::String(s) => Ok(s.clone()), + other => Err(StdlibError::TypeError { + expected: "String".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Helper to extract an integer argument +pub fn expect_int(value: &Value, _arg_name: &str) -> Result { + match value { + Value::Int(n) => Ok(*n), + other => Err(StdlibError::TypeError { + expected: "Int".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Helper to extract a float argument +pub fn expect_float(value: &Value, _arg_name: &str) -> Result { + match value { + Value::Float(n) => Ok(*n), + Value::Int(n) => Ok(*n as f64), + other => Err(StdlibError::TypeError { + expected: "Float".to_string(), + got: format!("{:?}", other), + }), + } +} + +/// Helper to extract a boolean argument +pub fn expect_bool(value: &Value, _arg_name: &str) -> Result { + match value { + Value::Bool(b) => Ok(*b), + other => Err(StdlibError::TypeError { + expected: "Bool".to_string(), + got: format!("{:?}", other), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_registry_creation() { + let registry = StdlibRegistry::new(); + assert!(registry.has("std.math.abs")); + assert!(registry.has("std.io.readFile")); + assert!(registry.has("std.json.parse")); + assert!(registry.has("std.time.now")); + assert!(!registry.has("nonexistent")); + } + + #[test] + fn test_check_arity() { + let args = vec![Value::Int(1), Value::Int(2)]; + assert!(check_arity(&args, 2).is_ok()); + assert!(check_arity(&args, 3).is_err()); + } +} diff --git a/src/stdlib/net.rs b/src/stdlib/net.rs new file mode 100644 index 0000000..8fcdba2 --- /dev/null +++ b/src/stdlib/net.rs @@ -0,0 +1,363 @@ +//! WokeLang Standard Library - Network Module +//! +//! HTTP and network operations that require explicit consent. + +use crate::interpreter::Value; +use crate::security::{Capability, CapabilityRegistry}; +use super::{check_arity, check_arity_range, expect_string, StdlibError}; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpStream; +use std::time::Duration; + +/// Helper to require network capability +fn require_network(host: &str, caps: &mut CapabilityRegistry) -> Result<(), StdlibError> { + let cap = Capability::Network(Some(host.to_string())); + if caps.request("stdlib", &cap).is_err() { + Err(StdlibError::PermissionDenied(format!( + "Network access denied: {}", + host + ))) + } else { + Ok(()) + } +} + +/// Parse a URL into components +fn parse_url(url: &str) -> Result<(String, String, u16, String), StdlibError> { + let url = url.trim(); + + // Remove protocol + let (is_https, rest) = if url.starts_with("https://") { + (true, &url[8..]) + } else if url.starts_with("http://") { + (false, &url[7..]) + } else { + (false, url) + }; + + // Split host and path + let (host_port, path) = match rest.find('/') { + Some(idx) => (&rest[..idx], &rest[idx..]), + None => (rest, "/"), + }; + + // Parse host and port + let (host, port) = match host_port.find(':') { + Some(idx) => { + let port: u16 = host_port[idx + 1..] + .parse() + .map_err(|_| StdlibError::NetworkError("Invalid port".to_string()))?; + (&host_port[..idx], port) + } + None => { + let default_port = if is_https { 443 } else { 80 }; + (host_port, default_port) + } + }; + + Ok(( + if is_https { "https" } else { "http" }.to_string(), + host.to_string(), + port, + path.to_string(), + )) +} + +/// Make an HTTP GET request +pub fn http_get(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 1, 2)?; + let url = expect_string(&args[0], "url")?; + + let (protocol, host, port, path) = parse_url(&url)?; + + // Check capability + require_network(&host, caps)?; + + // For HTTPS, we can't do it without TLS library - return error + if protocol == "https" { + return Err(StdlibError::NetworkError( + "HTTPS not supported without TLS library. Use HTTP or compile with TLS support.".to_string(), + )); + } + + // Make HTTP request + let response = http_request(&host, port, "GET", &path, None, None)?; + Ok(Value::String(response)) +} + +/// Make an HTTP POST request +pub fn http_post(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity_range(args, 2, 3)?; + let url = expect_string(&args[0], "url")?; + let body = expect_string(&args[1], "body")?; + + let content_type = if args.len() > 2 { + expect_string(&args[2], "content_type")? + } else { + "application/json".to_string() + }; + + let (protocol, host, port, path) = parse_url(&url)?; + + // Check capability + require_network(&host, caps)?; + + // For HTTPS, we can't do it without TLS library + if protocol == "https" { + return Err(StdlibError::NetworkError( + "HTTPS not supported without TLS library".to_string(), + )); + } + + // Make HTTP request + let response = http_request(&host, port, "POST", &path, Some(&body), Some(&content_type))?; + Ok(Value::String(response)) +} + +/// Download a file from a URL +pub fn download(args: &[Value], caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let url = expect_string(&args[0], "url")?; + let dest_path = expect_string(&args[1], "path")?; + + let (protocol, host, port, path) = parse_url(&url)?; + + // Check network capability + require_network(&host, caps)?; + + // Check file write capability + let file_cap = Capability::FileWrite(Some(std::path::PathBuf::from(&dest_path))); + if caps.request("stdlib", &file_cap).is_err() { + return Err(StdlibError::PermissionDenied(format!( + "File write access denied: {}", + dest_path + ))); + } + + // For HTTPS, we can't do it without TLS library + if protocol == "https" { + return Err(StdlibError::NetworkError( + "HTTPS not supported without TLS library".to_string(), + )); + } + + // Make HTTP request + let response = http_request_binary(&host, port, "GET", &path)?; + + // Write to file + std::fs::write(&dest_path, response) + .map_err(|e| StdlibError::IoError(e.to_string()))?; + + Ok(Value::Bool(true)) +} + +/// Make an HTTP request and return the response body as string +fn http_request( + host: &str, + port: u16, + method: &str, + path: &str, + body: Option<&str>, + content_type: Option<&str>, +) -> Result { + let bytes = http_request_binary_with_body(host, port, method, path, body, content_type)?; + String::from_utf8(bytes).map_err(|e| StdlibError::NetworkError(e.to_string())) +} + +/// Make an HTTP request and return the response body as bytes +fn http_request_binary(host: &str, port: u16, method: &str, path: &str) -> Result, StdlibError> { + http_request_binary_with_body(host, port, method, path, None, None) +} + +/// Make an HTTP request with optional body +fn http_request_binary_with_body( + host: &str, + port: u16, + method: &str, + path: &str, + body: Option<&str>, + content_type: Option<&str>, +) -> Result, StdlibError> { + // Connect + let addr = format!("{}:{}", host, port); + let mut stream = TcpStream::connect(&addr) + .map_err(|e| StdlibError::NetworkError(format!("Connection failed: {}", e)))?; + + stream + .set_read_timeout(Some(Duration::from_secs(30))) + .ok(); + stream + .set_write_timeout(Some(Duration::from_secs(30))) + .ok(); + + // Build request + let mut request = format!("{} {} HTTP/1.1\r\n", method, path); + request.push_str(&format!("Host: {}\r\n", host)); + request.push_str("User-Agent: WokeLang/1.0\r\n"); + request.push_str("Connection: close\r\n"); + + if let Some(body_content) = body { + let content_type = content_type.unwrap_or("application/octet-stream"); + request.push_str(&format!("Content-Type: {}\r\n", content_type)); + request.push_str(&format!("Content-Length: {}\r\n", body_content.len())); + request.push_str("\r\n"); + request.push_str(body_content); + } else { + request.push_str("\r\n"); + } + + // Send request + stream + .write_all(request.as_bytes()) + .map_err(|e| StdlibError::NetworkError(format!("Send failed: {}", e)))?; + + // Read response + let mut reader = BufReader::new(&stream); + + // Read status line + let mut status_line = String::new(); + reader + .read_line(&mut status_line) + .map_err(|e| StdlibError::NetworkError(format!("Read failed: {}", e)))?; + + // Parse status + let status_parts: Vec<&str> = status_line.split_whitespace().collect(); + if status_parts.len() < 2 { + return Err(StdlibError::NetworkError("Invalid HTTP response".to_string())); + } + let status_code: u16 = status_parts[1] + .parse() + .map_err(|_| StdlibError::NetworkError("Invalid status code".to_string()))?; + + // Read headers + let mut content_length: Option = None; + let mut chunked = false; + + loop { + let mut header = String::new(); + reader + .read_line(&mut header) + .map_err(|e| StdlibError::NetworkError(format!("Read header failed: {}", e)))?; + + let header = header.trim(); + if header.is_empty() { + break; + } + + let lower = header.to_lowercase(); + if lower.starts_with("content-length:") { + content_length = header[15..].trim().parse().ok(); + } else if lower.starts_with("transfer-encoding:") && lower.contains("chunked") { + chunked = true; + } + } + + // Read body + let body = if chunked { + read_chunked_body(&mut reader)? + } else if let Some(len) = content_length { + let mut buf = vec![0u8; len]; + reader + .read_exact(&mut buf) + .map_err(|e| StdlibError::NetworkError(format!("Read body failed: {}", e)))?; + buf + } else { + // Read until connection closes + let mut buf = Vec::new(); + reader + .read_to_end(&mut buf) + .map_err(|e| StdlibError::NetworkError(format!("Read body failed: {}", e)))?; + buf + }; + + // Check for error status codes + if status_code >= 400 { + let body_str = String::from_utf8_lossy(&body); + return Err(StdlibError::NetworkError(format!( + "HTTP {} error: {}", + status_code, body_str + ))); + } + + Ok(body) +} + +/// Read chunked transfer encoding body +fn read_chunked_body(reader: &mut R) -> Result, StdlibError> { + let mut body = Vec::new(); + + loop { + // Read chunk size line + let mut size_line = String::new(); + reader + .read_line(&mut size_line) + .map_err(|e| StdlibError::NetworkError(format!("Read chunk size failed: {}", e)))?; + + let size = usize::from_str_radix(size_line.trim(), 16) + .map_err(|_| StdlibError::NetworkError("Invalid chunk size".to_string()))?; + + if size == 0 { + // Read trailing CRLF + let mut trailing = String::new(); + reader.read_line(&mut trailing).ok(); + break; + } + + // Read chunk data + let mut chunk = vec![0u8; size]; + reader + .read_exact(&mut chunk) + .map_err(|e| StdlibError::NetworkError(format!("Read chunk failed: {}", e)))?; + body.extend(chunk); + + // Read trailing CRLF + let mut crlf = [0u8; 2]; + reader.read_exact(&mut crlf).ok(); + } + + Ok(body) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_url() { + let (proto, host, port, path) = parse_url("http://example.com/path").unwrap(); + assert_eq!(proto, "http"); + assert_eq!(host, "example.com"); + assert_eq!(port, 80); + assert_eq!(path, "/path"); + + let (proto, host, port, path) = parse_url("https://example.com:8443/api/v1").unwrap(); + assert_eq!(proto, "https"); + assert_eq!(host, "example.com"); + assert_eq!(port, 8443); + assert_eq!(path, "/api/v1"); + + let (proto, host, port, path) = parse_url("example.com").unwrap(); + assert_eq!(host, "example.com"); + assert_eq!(port, 80); + assert_eq!(path, "/"); + } + + // Network tests would require a test server, so we just test URL parsing + #[test] + fn test_require_network_denied() { + let mut caps = CapabilityRegistry::new(); + caps.set_interactive(false); + caps.set_default_consent(false); + + let result = require_network("example.com", &mut caps); + assert!(result.is_err()); + } + + #[test] + fn test_require_network_granted() { + let mut caps = CapabilityRegistry::permissive(); + + let result = require_network("example.com", &mut caps); + assert!(result.is_ok()); + } +} diff --git a/src/stdlib/time.rs b/src/stdlib/time.rs new file mode 100644 index 0000000..c09997e --- /dev/null +++ b/src/stdlib/time.rs @@ -0,0 +1,383 @@ +//! WokeLang Standard Library - Time Module +//! +//! Date and time handling functions. + +use crate::interpreter::Value; +use crate::security::CapabilityRegistry; +use super::{check_arity, expect_int, expect_string, StdlibError}; +use std::collections::HashMap; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +// Thread-local storage for elapsed time tracking +thread_local! { + static START_TIMES: std::cell::RefCell> = std::cell::RefCell::new(HashMap::new()); +} + +/// Get current timestamp as milliseconds since epoch +pub fn now(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 0)?; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0); + + Ok(Value::Int(timestamp)) +} + +/// Get current timestamp as seconds since epoch +pub fn timestamp(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 0)?; + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + + Ok(Value::Int(timestamp)) +} + +/// Format a timestamp to a string +/// format(timestamp, format_string) +/// Format tokens: %Y=year, %m=month, %d=day, %H=hour, %M=minute, %S=second +pub fn format(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let timestamp_ms = expect_int(&args[0], "timestamp")?; + let format_str = expect_string(&args[1], "format")?; + + // Convert milliseconds to components + let total_secs = timestamp_ms / 1000; + let (year, month, day, hour, minute, second) = timestamp_to_components(total_secs); + + // Simple format replacement + let result = format_str + .replace("%Y", &format!("{:04}", year)) + .replace("%m", &format!("{:02}", month)) + .replace("%d", &format!("{:02}", day)) + .replace("%H", &format!("{:02}", hour)) + .replace("%M", &format!("{:02}", minute)) + .replace("%S", &format!("{:02}", second)); + + Ok(Value::String(result)) +} + +/// Parse a date string to timestamp +/// parse(date_string, format_string) +pub fn parse(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let date_str = expect_string(&args[0], "date")?; + let format_str = expect_string(&args[1], "format")?; + + // Simple parsing for ISO-like formats + let result = parse_date_string(&date_str, &format_str)?; + Ok(Value::Int(result)) +} + +/// Sleep for a given number of milliseconds +pub fn sleep(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 1)?; + let ms = expect_int(&args[0], "milliseconds")?; + + if ms > 0 { + std::thread::sleep(Duration::from_millis(ms as u64)); + } + + Ok(Value::Unit) +} + +/// Start or get elapsed time for a named timer +/// elapsed("start", "timer_name") - starts a timer +/// elapsed("stop", "timer_name") - returns elapsed milliseconds +pub fn elapsed(args: &[Value], _caps: &mut CapabilityRegistry) -> Result { + check_arity(args, 2)?; + let action = expect_string(&args[0], "action")?; + let name = expect_string(&args[1], "name")?; + + match action.as_str() { + "start" => { + START_TIMES.with(|times| { + times.borrow_mut().insert(name, Instant::now()); + }); + Ok(Value::Unit) + } + "stop" | "get" => { + let elapsed = START_TIMES.with(|times| { + times + .borrow() + .get(&name) + .map(|start| start.elapsed().as_millis() as i64) + }); + + match elapsed { + Some(ms) => Ok(Value::Int(ms)), + None => Err(StdlibError::RuntimeError(format!( + "Timer '{}' not started", + name + ))), + } + } + "reset" => { + START_TIMES.with(|times| { + times.borrow_mut().remove(&name); + }); + Ok(Value::Unit) + } + _ => Err(StdlibError::RuntimeError(format!( + "Unknown timer action: {}. Use 'start', 'stop', 'get', or 'reset'", + action + ))), + } +} + +/// Convert timestamp (seconds since epoch) to date components +fn timestamp_to_components(total_secs: i64) -> (i32, u32, u32, u32, u32, u32) { + // Simple conversion (ignores leap seconds) + let days_since_epoch = total_secs / 86400; + let time_of_day = total_secs % 86400; + + let hour = (time_of_day / 3600) as u32; + let minute = ((time_of_day % 3600) / 60) as u32; + let second = (time_of_day % 60) as u32; + + // Convert days to year/month/day + // Using a simplified algorithm (not accounting for all edge cases) + let (year, month, day) = days_to_ymd(days_since_epoch as i32); + + (year, month, day, hour, minute, second) +} + +/// Convert days since epoch to year/month/day +fn days_to_ymd(days: i32) -> (i32, u32, u32) { + // Days from 1970-01-01 + let mut remaining = days; + let mut year = 1970; + + // Find year + loop { + let days_in_year = if is_leap_year(year) { 366 } else { 365 }; + if remaining < days_in_year { + break; + } + remaining -= days_in_year; + year += 1; + } + + // Find month + let days_in_months = if is_leap_year(year) { + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + let mut month = 1u32; + for days_in_month in days_in_months.iter() { + if remaining < *days_in_month { + break; + } + remaining -= days_in_month; + month += 1; + } + + let day = (remaining + 1) as u32; + + (year, month, day) +} + +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +/// Parse a date string given a format +fn parse_date_string(date_str: &str, format_str: &str) -> Result { + // Support common formats + let date_str = date_str.trim(); + + // Try ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS + if format_str.contains("%Y") && format_str.contains("%m") && format_str.contains("%d") { + let parts: Vec<&str> = date_str.split(|c| c == '-' || c == 'T' || c == ':' || c == ' ') + .collect(); + + if parts.len() >= 3 { + let year: i32 = parts[0] + .parse() + .map_err(|_| StdlibError::ParseError("Invalid year".to_string()))?; + let month: u32 = parts[1] + .parse() + .map_err(|_| StdlibError::ParseError("Invalid month".to_string()))?; + let day: u32 = parts[2] + .parse() + .map_err(|_| StdlibError::ParseError("Invalid day".to_string()))?; + + let hour: u32 = parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(0); + let minute: u32 = parts.get(4).and_then(|s| s.parse().ok()).unwrap_or(0); + let second: u32 = parts.get(5).and_then(|s| s.parse().ok()).unwrap_or(0); + + let timestamp = ymd_to_timestamp(year, month, day, hour, minute, second); + return Ok(timestamp * 1000); // Return milliseconds + } + } + + Err(StdlibError::ParseError(format!( + "Could not parse date '{}' with format '{}'", + date_str, format_str + ))) +} + +/// Convert year/month/day to timestamp +fn ymd_to_timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> i64 { + // Days from 1970 to start of year + let mut days: i64 = 0; + + if year >= 1970 { + for y in 1970..year { + days += if is_leap_year(y) { 366 } else { 365 }; + } + } else { + for y in year..1970 { + days -= if is_leap_year(y) { 366 } else { 365 }; + } + } + + // Days in current year + let days_in_months = if is_leap_year(year) { + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + for m in 0..(month - 1) as usize { + days += days_in_months[m] as i64; + } + days += (day - 1) as i64; + + // Convert to seconds + days * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_caps() -> CapabilityRegistry { + CapabilityRegistry::permissive() + } + + #[test] + fn test_now() { + let mut caps = test_caps(); + let result = now(&[], &mut caps).unwrap(); + match result { + Value::Int(ts) => assert!(ts > 0), + _ => panic!("Expected integer"), + } + } + + #[test] + fn test_timestamp() { + let mut caps = test_caps(); + let result = timestamp(&[], &mut caps).unwrap(); + match result { + Value::Int(ts) => assert!(ts > 0), + _ => panic!("Expected integer"), + } + } + + #[test] + fn test_format() { + let mut caps = test_caps(); + // 2024-01-15 12:30:45 UTC + let ts = 1705322445000i64; // milliseconds + + let result = format( + &[Value::Int(ts), Value::String("%Y-%m-%d".to_string())], + &mut caps, + ) + .unwrap(); + + assert_eq!(result, Value::String("2024-01-15".to_string())); + } + + #[test] + fn test_parse() { + let mut caps = test_caps(); + + let result = parse( + &[ + Value::String("2024-01-15".to_string()), + Value::String("%Y-%m-%d".to_string()), + ], + &mut caps, + ) + .unwrap(); + + match result { + Value::Int(ts) => { + // Should be midnight UTC on 2024-01-15 + assert!(ts > 0); + } + _ => panic!("Expected integer"), + } + } + + #[test] + fn test_sleep() { + let mut caps = test_caps(); + let start = Instant::now(); + + sleep(&[Value::Int(50)], &mut caps).unwrap(); + + let elapsed = start.elapsed().as_millis(); + assert!(elapsed >= 50); + } + + #[test] + fn test_elapsed() { + let mut caps = test_caps(); + + // Start timer + elapsed( + &[ + Value::String("start".to_string()), + Value::String("test_timer".to_string()), + ], + &mut caps, + ) + .unwrap(); + + // Sleep a bit + std::thread::sleep(Duration::from_millis(20)); + + // Get elapsed + let result = elapsed( + &[ + Value::String("stop".to_string()), + Value::String("test_timer".to_string()), + ], + &mut caps, + ) + .unwrap(); + + match result { + Value::Int(ms) => assert!(ms >= 20), + _ => panic!("Expected integer"), + } + + // Reset + elapsed( + &[ + Value::String("reset".to_string()), + Value::String("test_timer".to_string()), + ], + &mut caps, + ) + .unwrap(); + } + + #[test] + fn test_days_to_ymd() { + // 1970-01-01 + assert_eq!(days_to_ymd(0), (1970, 1, 1)); + // 2000-01-01 (10957 days from epoch) + assert_eq!(days_to_ymd(10957), (2000, 1, 1)); + } +}