From 52ad5f8781e30b8b7e87a3e5f8fa3395fae89c50 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 09:49:09 -0500 Subject: [PATCH] feat: implement Random and Time built-in effects Add Random and Time effects for random number generation and time-based operations. These effects can be used in any effectful code block. Random effect operations: - Random.int(min, max) - random integer in range [min, max] - Random.float() - random float in range [0.0, 1.0) - Random.bool() - random boolean Time effect operations: - Time.now() - current Unix timestamp in milliseconds - Time.sleep(ms) - sleep for specified milliseconds Changes: - Add rand crate dependency - Add Random and Time effect definitions to types.rs - Add effects to built-in effects list in typechecker - Implement effect handlers in interpreter - Add 4 new tests for Random and Time effects - Add examples/random.lux demonstrating usage Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 79 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + examples/random.lux | 41 +++++++++++++++++++++++ src/interpreter.rs | 42 ++++++++++++++++++++++++ src/main.rs | 44 +++++++++++++++++++++++++ src/typechecker.rs | 4 +-- src/types.rs | 50 ++++++++++++++++++++++++++++ 7 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 examples/random.lux diff --git a/Cargo.lock b/Cargo.lock index 044636b..d5e2a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.1" @@ -358,6 +369,7 @@ version = "0.1.0" dependencies = [ "lsp-server", "lsp-types", + "rand", "rustyline", "serde", "serde_json", @@ -413,6 +425,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -457,6 +478,36 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "rustix" version = "1.1.3" @@ -593,7 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -678,6 +729,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -944,6 +1001,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 860c3db..78cb421 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ lsp-server = "0.7" lsp-types = "0.94" serde = { version = "1", features = ["derive"] } serde_json = "1" +rand = "0.8" [dev-dependencies] tempfile = "3" diff --git a/examples/random.lux b/examples/random.lux new file mode 100644 index 0000000..b957922 --- /dev/null +++ b/examples/random.lux @@ -0,0 +1,41 @@ +// Demonstrating Random and Time effects in Lux +// +// Expected output (values will vary): +// Rolling dice... +// Die 1: +// Die 2: +// Die 3: +// Coin flip: +// Random float: <0.0-1.0> +// Current time: + +// Roll a single die (1-6) +fn rollDie(): Int with {Random} = Random.int(1, 6) + +// Roll multiple dice and print results +fn rollDice(count: Int): Unit with {Random, Console} = { + if count > 0 then { + let value = rollDie() + Console.print("Die " + toString(4 - count) + ": " + toString(value)) + rollDice(count - 1) + } else { + () + } +} + +// Main function demonstrating random effects +fn main(): Unit with {Random, Console, Time} = { + Console.print("Rolling dice...") + rollDice(3) + + let coin = Random.bool() + Console.print("Coin flip: " + toString(coin)) + + let f = Random.float() + Console.print("Random float: " + toString(f)) + + let now = Time.now() + Console.print("Current time: " + toString(now)) +} + +let output = run main() with {} diff --git a/src/interpreter.rs b/src/interpreter.rs index e1173f0..492dd02 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -4,6 +4,7 @@ use crate::ast::*; use crate::diagnostics::{Diagnostic, Severity}; +use rand::Rng; use std::cell::RefCell; use std::collections::HashMap; use std::fmt; @@ -2177,6 +2178,47 @@ impl Interpreter { ("Reader", "ask") => { Ok(self.builtin_reader.borrow().clone()) } + ("Random", "int") => { + let min = match request.args.first() { + Some(Value::Int(n)) => *n, + _ => 0, + }; + let max = match request.args.get(1) { + Some(Value::Int(n)) => *n, + _ => i64::MAX, + }; + let mut rng = rand::thread_rng(); + let value = rng.gen_range(min..=max); + Ok(Value::Int(value)) + } + ("Random", "float") => { + let mut rng = rand::thread_rng(); + let value: f64 = rng.gen(); + Ok(Value::Float(value)) + } + ("Random", "bool") => { + let mut rng = rand::thread_rng(); + let value: bool = rng.gen(); + Ok(Value::Bool(value)) + } + ("Time", "now") => { + use std::time::{SystemTime, UNIX_EPOCH}; + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + let millis = duration.as_millis() as i64; + Ok(Value::Int(millis)) + } + ("Time", "sleep") => { + use std::thread; + use std::time::Duration; + let ms = match request.args.first() { + Some(Value::Int(n)) => *n as u64, + _ => 0, + }; + thread::sleep(Duration::from_millis(ms)); + Ok(Value::Unit) + } _ => Err(RuntimeError { message: format!( "Unhandled effect operation: {}.{}", diff --git a/src/main.rs b/src/main.rs index ca029db..b7e800a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1248,6 +1248,50 @@ c")"#; assert_eq!(result, "5"); assert_eq!(final_state, "6"); } + + #[test] + fn test_random_int() { + let source = r#" + fn getRandomInt(): Int with {Random} = Random.int(1, 10) + let result = run getRandomInt() with {} + "#; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + let num: i64 = result.parse().expect("Should be an integer"); + assert!(num >= 1 && num <= 10, "Random int should be in range 1-10, got {}", num); + } + + #[test] + fn test_random_float() { + let source = r#" + fn getRandomFloat(): Float with {Random} = Random.float() + let result = run getRandomFloat() with {} + "#; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + let num: f64 = result.parse().expect("Should be a float"); + assert!(num >= 0.0 && num < 1.0, "Random float should be in range [0, 1), got {}", num); + } + + #[test] + fn test_random_bool() { + let source = r#" + fn getRandomBool(): Bool with {Random} = Random.bool() + let result = run getRandomBool() with {} + "#; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + assert!(result == "true" || result == "false", "Random bool should be true or false, got {}", result); + } + + #[test] + fn test_time_now() { + let source = r#" + fn getTime(): Int with {Time} = Time.now() + let result = run getTime() with {} + "#; + let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap(); + let timestamp: i64 = result.parse().expect("Should be a timestamp"); + // Timestamp should be a reasonable Unix time in milliseconds (after 2020) + assert!(timestamp > 1577836800000, "Timestamp should be after 2020"); + } } // Diagnostic rendering tests diff --git a/src/typechecker.rs b/src/typechecker.rs index b2f1fdc..9a6ca16 100644 --- a/src/typechecker.rs +++ b/src/typechecker.rs @@ -935,7 +935,7 @@ impl TypeChecker { } // Built-in effects are always available - let builtin_effects = ["Console", "Fail", "State", "Reader"]; + let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time"]; let is_builtin = builtin_effects.contains(&effect.name.as_str()); // Track this effect for inference @@ -1440,7 +1440,7 @@ impl TypeChecker { // Built-in effects are always available in run blocks (they have runtime implementations) let builtin_effects: EffectSet = - EffectSet::from_iter(["Console", "Fail", "State", "Reader"].iter().map(|s| s.to_string())); + EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time"].iter().map(|s| s.to_string())); // Extend current effects with handled ones and built-in effects let combined = self.current_effects.union(&handled_effects).union(&builtin_effects); diff --git a/src/types.rs b/src/types.rs index 26fe01a..8ce6814 100644 --- a/src/types.rs +++ b/src/types.rs @@ -802,6 +802,56 @@ impl TypeEnv { }, ); + // Add Random effect + env.effects.insert( + "Random".to_string(), + EffectDef { + name: "Random".to_string(), + type_params: Vec::new(), + operations: vec![ + EffectOpDef { + name: "int".to_string(), + params: vec![ + ("min".to_string(), Type::Int), + ("max".to_string(), Type::Int), + ], + return_type: Type::Int, + }, + EffectOpDef { + name: "float".to_string(), + params: Vec::new(), + return_type: Type::Float, + }, + EffectOpDef { + name: "bool".to_string(), + params: Vec::new(), + return_type: Type::Bool, + }, + ], + }, + ); + + // Add Time effect + env.effects.insert( + "Time".to_string(), + EffectDef { + name: "Time".to_string(), + type_params: Vec::new(), + operations: vec![ + EffectOpDef { + name: "now".to_string(), + params: Vec::new(), + return_type: Type::Int, // Unix timestamp in milliseconds + }, + EffectOpDef { + name: "sleep".to_string(), + params: vec![("ms".to_string(), Type::Int)], + return_type: Type::Unit, + }, + ], + }, + ); + // Add Some and Ok, Err constructors // Some : fn(a) -> Option let a = Type::var();