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 <noreply@anthropic.com>
This commit is contained in:
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -127,6 +127,17 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -358,6 +369,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"lsp-server",
|
"lsp-server",
|
||||||
"lsp-types",
|
"lsp-types",
|
||||||
|
"rand",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -413,6 +425,15 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -457,6 +478,36 @@ dependencies = [
|
|||||||
"nibble_vec",
|
"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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -593,7 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom",
|
"getrandom 0.4.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -678,6 +729,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.2+wasi-0.2.9"
|
version = "1.0.2+wasi-0.2.9"
|
||||||
@@ -944,6 +1001,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"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]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ lsp-server = "0.7"
|
|||||||
lsp-types = "0.94"
|
lsp-types = "0.94"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
41
examples/random.lux
Normal file
41
examples/random.lux
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Demonstrating Random and Time effects in Lux
|
||||||
|
//
|
||||||
|
// Expected output (values will vary):
|
||||||
|
// Rolling dice...
|
||||||
|
// Die 1: <random 1-6>
|
||||||
|
// Die 2: <random 1-6>
|
||||||
|
// Die 3: <random 1-6>
|
||||||
|
// Coin flip: <true/false>
|
||||||
|
// Random float: <0.0-1.0>
|
||||||
|
// Current time: <timestamp>
|
||||||
|
|
||||||
|
// 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 {}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::diagnostics::{Diagnostic, Severity};
|
use crate::diagnostics::{Diagnostic, Severity};
|
||||||
|
use rand::Rng;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -2177,6 +2178,47 @@ impl Interpreter {
|
|||||||
("Reader", "ask") => {
|
("Reader", "ask") => {
|
||||||
Ok(self.builtin_reader.borrow().clone())
|
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 {
|
_ => Err(RuntimeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unhandled effect operation: {}.{}",
|
"Unhandled effect operation: {}.{}",
|
||||||
|
|||||||
44
src/main.rs
44
src/main.rs
@@ -1248,6 +1248,50 @@ c")"#;
|
|||||||
assert_eq!(result, "5");
|
assert_eq!(result, "5");
|
||||||
assert_eq!(final_state, "6");
|
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
|
// Diagnostic rendering tests
|
||||||
|
|||||||
@@ -935,7 +935,7 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Built-in effects are always available
|
// 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());
|
let is_builtin = builtin_effects.contains(&effect.name.as_str());
|
||||||
|
|
||||||
// Track this effect for inference
|
// Track this effect for inference
|
||||||
@@ -1440,7 +1440,7 @@ impl TypeChecker {
|
|||||||
|
|
||||||
// Built-in effects are always available in run blocks (they have runtime implementations)
|
// Built-in effects are always available in run blocks (they have runtime implementations)
|
||||||
let builtin_effects: EffectSet =
|
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
|
// Extend current effects with handled ones and built-in effects
|
||||||
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
|
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
|
||||||
|
|||||||
50
src/types.rs
50
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
|
// Add Some and Ok, Err constructors
|
||||||
// Some : fn(a) -> Option<a>
|
// Some : fn(a) -> Option<a>
|
||||||
let a = Type::var();
|
let a = Type::var();
|
||||||
|
|||||||
Reference in New Issue
Block a user