1use ecow::EcoString;
2use rand::rngs::{ChaCha12Rng, SysRng};
3use rand::{Rng, SeedableRng, TryRng};
4use std::fmt::{self, Display, Formatter};
5
6use super::IoOutput;
7
8pub struct GleamStdlibRunState<Io = Vec<IoOutput>> {
10 random: ChaCha12Rng,
11 io: Io,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct GleamStdlibRunStateError {
17 reason: EcoString,
18}
19
20impl GleamStdlibRunState {
21 pub fn from_seed(seed: [u8; 32]) -> Self {
23 Self::from_seed_with_io(seed, Vec::new())
24 }
25
26 pub fn try_from_entropy() -> Result<Self, GleamStdlibRunStateError> {
28 Self::try_from_entropy_with_io(Vec::new())
29 }
30
31 pub fn io_outputs(&self) -> &[IoOutput] {
33 &self.io
34 }
35
36 pub fn take_io_outputs(&mut self) -> Vec<IoOutput> {
38 std::mem::take(&mut self.io)
39 }
40}
41
42impl<Io> GleamStdlibRunState<Io> {
43 pub fn from_seed_with_io(seed: [u8; 32], io: Io) -> Self {
45 Self {
46 random: ChaCha12Rng::from_seed(seed),
47 io,
48 }
49 }
50
51 pub fn try_from_entropy_with_io(io: Io) -> Result<Self, GleamStdlibRunStateError> {
53 Self::try_from_seed_source(io, |seed| SysRng.try_fill_bytes(seed))
54 }
55
56 pub(super) fn random_float(&mut self) -> f64 {
57 const SCALE: f64 = 1.0 / ((1u64 << 53) as f64);
58
59 ((self.random.next_u64() >> 11) as f64) * SCALE
60 }
61
62 pub(super) fn io_sink(&mut self) -> &mut Io {
63 &mut self.io
64 }
65
66 fn try_from_seed_source<Error>(
67 io: Io,
68 fill: impl FnOnce(&mut [u8; 32]) -> Result<(), Error>,
69 ) -> Result<Self, GleamStdlibRunStateError>
70 where
71 Error: Display,
72 {
73 let mut seed = [0; 32];
74 fill(&mut seed)
75 .map(|()| Self::from_seed_with_io(seed, io))
76 .map_err(|error| GleamStdlibRunStateError {
77 reason: error.to_string().into(),
78 })
79 }
80}
81
82impl GleamStdlibRunStateError {
83 pub fn reason(&self) -> &str {
85 &self.reason
86 }
87}
88
89impl Display for GleamStdlibRunStateError {
90 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
91 write!(
92 formatter,
93 "could not initialize Gleam standard-library random state: {}",
94 self.reason
95 )
96 }
97}
98
99impl std::error::Error for GleamStdlibRunStateError {}
100
101#[cfg(test)]
102mod tests {
103 use super::{GleamStdlibRunState, GleamStdlibRunStateError};
104 use crate::{IoOutput, IoSink, IoStream};
105 use std::fmt::{self, Display, Formatter};
106
107 #[derive(Debug)]
108 struct RejectedEntropyError;
109
110 impl Display for RejectedEntropyError {
111 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
112 formatter.write_str("entropy unavailable")
113 }
114 }
115
116 impl std::error::Error for RejectedEntropyError {}
117
118 #[test]
119 fn explicit_seeds_are_reproducible_and_advance_independently() {
120 let mut first = GleamStdlibRunState::from_seed([7; 32]);
121 let mut second = GleamStdlibRunState::from_seed([7; 32]);
122
123 let first_value = first.random_float();
124 assert_eq!(first_value, second.random_float());
125 let first_next = first.random_float();
126 assert_ne!(first_next, first_value);
127 assert_eq!(first_next, second.random_float());
128 assert!((0.0..1.0).contains(&first_value));
129 assert!(first.io_outputs().is_empty());
130 assert!(second.io_outputs().is_empty());
131 }
132
133 #[test]
134 fn io_outputs_accumulate_and_can_be_taken() {
135 let mut state = GleamStdlibRunState::from_seed([8; 32]);
136 state
137 .io_sink()
138 .emit(IoOutput::new(IoStream::Stdout, "first".into()));
139 state
140 .io_sink()
141 .emit(IoOutput::new(IoStream::Stderr, "second".into()));
142
143 assert_eq!(
144 state
145 .io_outputs()
146 .iter()
147 .map(|output| (output.stream(), output.text().as_str()))
148 .collect::<Vec<_>>(),
149 [(IoStream::Stdout, "first"), (IoStream::Stderr, "second")],
150 );
151 assert_eq!(state.take_io_outputs().len(), 2);
152 assert!(state.io_outputs().is_empty());
153 }
154
155 #[test]
156 fn entropy_failure_preserves_an_owned_reason() {
157 let error = GleamStdlibRunState::try_from_seed_source(Vec::<IoOutput>::new(), |_| {
158 Err(RejectedEntropyError)
159 })
160 .err()
161 .expect("rejected entropy should fail");
162
163 assert_eq!(error.reason(), "entropy unavailable");
164 assert_eq!(
165 error,
166 GleamStdlibRunStateError {
167 reason: "entropy unavailable".into(),
168 },
169 );
170 assert_eq!(
171 error.to_string(),
172 "could not initialize Gleam standard-library random state: entropy unavailable",
173 );
174 }
175
176 #[test]
177 fn system_entropy_constructs_an_independent_state() {
178 let mut state = GleamStdlibRunState::try_from_entropy()
179 .expect("system entropy should be available in the test environment");
180
181 assert!((0.0..1.0).contains(&state.random_float()));
182 assert!(state.io_outputs().is_empty());
183 }
184
185 #[test]
186 fn caller_owned_io_is_preserved_by_seeded_and_entropy_construction() {
187 let state = GleamStdlibRunState::from_seed_with_io([9; 32], String::from("sink"));
188 assert_eq!(state.io, "sink");
189
190 let state = GleamStdlibRunState::try_from_entropy_with_io(String::from("entropy sink"))
191 .expect("system entropy should initialize caller-owned IO state");
192 assert_eq!(state.io, "entropy sink");
193 }
194}