Skip to main content

aver/services/
random.rs

1/// Random service — effectful random number generation.
2///
3/// Methods:
4///   Random.int(min, max)  — random Int in [min, max] inclusive; ! [Random]
5///   Random.float()        — random Float in [0.0, 1.0); ! [Random]
6///
7/// Backed by `aver_rt::random` (OS entropy via `rand` crate).
8use std::collections::HashMap;
9use std::sync::Arc as Rc;
10
11use crate::nan_value::{Arena, NanIntExt, NanValue};
12use crate::value::{RuntimeError, Value};
13
14pub fn register(global: &mut HashMap<String, Value>) {
15    let mut members = HashMap::new();
16    for method in &["int", "float"] {
17        members.insert(
18            method.to_string(),
19            Value::Builtin(format!("Random.{}", method)),
20        );
21    }
22    global.insert(
23        "Random".to_string(),
24        Value::Namespace {
25            name: "Random".to_string(),
26            members,
27        },
28    );
29}
30
31pub const DECLARED_EFFECTS: &[&str] = &["Random.int", "Random.float"];
32
33pub fn effects(name: &str) -> &'static [&'static str] {
34    match name {
35        "Random.int" => &["Random.int"],
36        "Random.float" => &["Random.float"],
37        _ => &[],
38    }
39}
40
41pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
42    match name {
43        "Random.int" => Some(random_int(args)),
44        "Random.float" => Some(random_float(args)),
45        _ => None,
46    }
47}
48
49fn random_int(args: &[Value]) -> Result<Value, RuntimeError> {
50    if args.len() != 2 {
51        return Err(RuntimeError::Error(format!(
52            "Random.int takes 2 arguments (min, max), got {}",
53            args.len()
54        )));
55    }
56    let Value::Int(min) = &args[0] else {
57        return Err(RuntimeError::Error(
58            "Random.int: first argument must be an Int".to_string(),
59        ));
60    };
61    let Value::Int(max) = &args[1] else {
62        return Err(RuntimeError::Error(
63            "Random.int: second argument must be an Int".to_string(),
64        ));
65    };
66
67    let (Some(min), Some(max)) = (min.to_i64(), max.to_i64()) else {
68        return Err(RuntimeError::Error(
69            "Random.int: bounds must fit a 64-bit integer".to_string(),
70        ));
71    };
72    match aver_rt::random::random_int(min, max) {
73        Ok(value) => Ok(Value::int(value)),
74        Err(msg) => Err(RuntimeError::Error(msg)),
75    }
76}
77
78fn random_float(args: &[Value]) -> Result<Value, RuntimeError> {
79    if !args.is_empty() {
80        return Err(RuntimeError::Error(format!(
81            "Random.float takes 0 arguments, got {}",
82            args.len()
83        )));
84    }
85    Ok(Value::Float(aver_rt::random::random_float()))
86}
87
88// ─── NanValue-native API ─────────────────────────────────────────────────────
89
90pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
91    let methods = &["int", "float"];
92    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
93    for method in methods {
94        let idx = arena.push_builtin(&format!("Random.{}", method));
95        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
96    }
97    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
98        name: Rc::from("Random"),
99        members,
100    });
101    global.insert("Random".to_string(), NanValue::new_namespace(ns_idx));
102}
103
104pub fn call_nv(
105    name: &str,
106    args: &[NanValue],
107    arena: &mut Arena,
108) -> Option<Result<NanValue, RuntimeError>> {
109    match name {
110        "Random.int" => Some(random_int_nv(args, arena)),
111        "Random.float" => Some(random_float_nv(args, arena)),
112        _ => None,
113    }
114}
115
116fn random_int_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
117    if args.len() != 2 {
118        return Err(RuntimeError::Error(format!(
119            "Random.int takes 2 arguments (min, max), got {}",
120            args.len()
121        )));
122    }
123    if !args[0].is_int() {
124        return Err(RuntimeError::Error(
125            "Random.int: first argument must be an Int".to_string(),
126        ));
127    }
128    if !args[1].is_int() {
129        return Err(RuntimeError::Error(
130            "Random.int: second argument must be an Int".to_string(),
131        ));
132    }
133    let (Some(min), Some(max)) = (
134        args[0].as_aver_int(arena).to_i64(),
135        args[1].as_aver_int(arena).to_i64(),
136    ) else {
137        return Err(RuntimeError::Error(
138            "Random.int: bounds must fit a 64-bit integer".to_string(),
139        ));
140    };
141    match aver_rt::random::random_int(min, max) {
142        Ok(value) => Ok(NanValue::new_int(value, arena)),
143        Err(msg) => Err(RuntimeError::Error(msg)),
144    }
145}
146
147fn random_float_nv(args: &[NanValue], _arena: &mut Arena) -> Result<NanValue, RuntimeError> {
148    if !args.is_empty() {
149        return Err(RuntimeError::Error(format!(
150            "Random.float takes 0 arguments, got {}",
151            args.len()
152        )));
153    }
154    Ok(NanValue::new_float(aver_rt::random::random_float()))
155}