Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ allowed-duplicate-crates = [
"objc2-foundation",
"proc-macro-utils", # TODO: can be resolved by updating leptos crates to 0.7.0
"serde_qs", # TODO: can be resolved by updating leptos crates to 0.7.0
"rand_core",
"rustix",
"linux-raw-sys", # needed by rustix@0.38
"thiserror",
Expand Down
24 changes: 20 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ js-sys = "0.3.69"
leptos = "0.6.11"
leptos_meta = {version = "0.6.11", features = ["csr"]}
leptos_router = {version = "0.6.11", features = ["csr"]}
rand = {version = "0.8.5", features = ["small_rng"]}
rand_xoshiro = "0.7.0"
serde = {version = "1", features = ["derive"]}
serde_json = "1.0.115"
unicode-segmentation = "1.10"
Expand Down Expand Up @@ -106,7 +106,6 @@ open = {version = "5", optional = true}
parking_lot = "0.12.1"
paste = "1.0.14"
pathdiff = "0.2.1"
rand.workspace = true
rawrrr = {version = "0.2.1", optional = true}
rayon = "1.9.0"
regex = "1.10.3"
Expand Down Expand Up @@ -181,8 +180,10 @@ wasm-bindgen = {workspace = true, optional = true}
web-sys = {version = "0.3.60", optional = true}

# Window dependencies
ahash = "0.8.12"
eframe = {version = "0.29.1", optional = true, features = ["persistence"]}
native-dialog = {version = "0.7.0", optional = true}
rand_xoshiro = "0.7.0"
rmp-serde = {version = "1.3.0", optional = true}

[features]
Expand Down
2 changes: 1 addition & 1 deletion site/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ js-sys.workspace = true
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
rand.workspace = true
rand_xoshiro.workspace = true
serde.workspace = true
serde_json.workspace = true
uiua = {path = "..", default-features = false, features = ["batteries", "web"]}
Expand Down
23 changes: 20 additions & 3 deletions site/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ use js_sys::Date;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use rand::prelude::*;
use rand_xoshiro::{
rand_core::{RngCore, SeedableRng},
Xoshiro256Plus,
};
use uiua::{now, ConstantDef, Primitive, SysOp};
use uiua_editor::{
binding_name_class, lang,
Expand Down Expand Up @@ -261,9 +264,23 @@ pub fn MainPage() -> impl IntoView {
let indices = if visits < 4 {
vec![0, rich_prims.len() - 3, rich_prims.len() - 1]
} else {
let mut rng = SmallRng::seed_from_u64(visits as u64);
let mut indices: Vec<usize> = (0..rich_prims.len()).collect();
indices.shuffle(&mut rng);
{
// Shuffle indices
let l = indices.len();
let upper = l.next_power_of_two();
let mut rng = Xoshiro256Plus::seed_from_u64(visits as u64);

for i in 0..indices.len() {
let index = loop {
let r = rng.next_u64() as usize % upper;
if r < l {
break r;
}
};
indices.swap(i, index);
}
Comment on lines +274 to +282
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Fisher-Yates shuffle implementation is incorrect. The random index should be selected from the range i..l (remaining elements), not 0..l. The current implementation can swap with already-shuffled elements, breaking uniformity. Change line 275-280 to generate a random number in the range [i, l) instead.

Copilot uses AI. Check for mistakes.
}
indices.truncate(3);
indices.sort_unstable();
indices
Expand Down
25 changes: 19 additions & 6 deletions src/algorithm/dyadic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ use core::f64;
use std::{
borrow::Cow,
cmp::Ordering,
hash::{DefaultHasher, Hash, Hasher},
hash::{Hash, Hasher},
iter::{once, repeat_n},
mem::{replace, swap, take},
};

use ahash::AHasher;
use bytemuck::allocation::cast_vec;
use ecow::{eco_vec, EcoVec};
use rand::prelude::*;
use rand_xoshiro::{
rand_core::{RngCore, SeedableRng},
Xoshiro256Plus,
};

#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use smallvec::SmallVec;
Expand Down Expand Up @@ -2109,10 +2114,10 @@ impl Value {
}
/// Generate randomly seeded arrays
pub fn gen(&self, seed: &Self, env: &Uiua) -> UiuaResult<Value> {
let mut hasher = DefaultHasher::new();
let mut hasher = AHasher::default();
seed.hash(&mut hasher);
let seed = hasher.finish();
let mut rng = SmallRng::seed_from_u64(seed);
let mut rng = Xoshiro256Plus::seed_from_u64(seed);

const SHAPE_REQ: &str = "Shape must be an array of natural \
numbers with at most rank 2";
Expand All @@ -2122,7 +2127,7 @@ impl Value {
let elem_count = validate_size::<f64>(shape.iter().copied(), env)?;
let mut data = eco_vec![0.0; elem_count];
for x in data.make_mut() {
*x = rng.gen();
*x = f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 1.0;
}
Ok(Array::new(shape, data))
};
Expand Down Expand Up @@ -2168,7 +2173,15 @@ impl Value {
0 => Err(env.error("Cannot pick random row of an empty array").fill()),
1 => Ok(self.row(0)),
len => {
let i = RNG.with_borrow_mut(|rng| rng.gen_range(0..len));
let i = RNG.with_borrow_mut(|rng| {
let upper = len.next_power_of_two();
loop {
let r = rng.next_u64() as usize % upper;
if r < len {
break r;
}
}
Comment on lines +2177 to +2183
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rejection sampling approach can be inefficient when len is just above a power of two. Consider using (rng.next_u64() as u128 * len as u128 >> 64) as usize for unbiased constant-time sampling.

Suggested change
let upper = len.next_power_of_two();
loop {
let r = rng.next_u64() as usize % upper;
if r < len {
break r;
}
}
((rng.next_u64() as u128 * len as u128) >> 64) as usize

Copilot uses AI. Check for mistakes.
});
Ok(self.row(i))
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/algorithm/monadic/sort.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{cmp::Ordering, ptr};

use ecow::EcoVec;
use rand::Rng;
use rand_xoshiro::rand_core::RngCore;
use rayon::prelude::*;

use crate::{algorithm::ArrayCmpSlice, random_with, val_as_arr, Array, ArrayValue, Value};
Expand Down Expand Up @@ -232,7 +232,16 @@ impl<T: ArrayValue> Array<T> {
let row_len = self.row_len();
let slice = self.data.as_mut_slice();
for i in (1..row_count).rev() {
let j = rng.gen_range(0..=i);
let j = {
let upper = i.next_power_of_two();
loop {
let r = rng.next_u64() as usize % upper;
if r <= i {
break r;
}
}
Comment on lines +236 to +242
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rejection sampling approach can be inefficient when i is just above a power of two (e.g., i=65 requires upper=128, rejecting ~49% of samples). For better performance, use (rng.next_u64() as u128 * (i as u128 + 1) >> 64) as usize which provides unbiased sampling in constant time.

Suggested change
let upper = i.next_power_of_two();
loop {
let r = rng.next_u64() as usize % upper;
if r <= i {
break r;
}
}
// Use constant-time unbiased sampling
((rng.next_u64() as u128 * (i as u128 + 1)) >> 64) as usize

Copilot uses AI. Check for mistakes.
};

if i == j {
continue;
}
Expand Down
13 changes: 9 additions & 4 deletions src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::{
};

use ecow::EcoVec;
use rand::prelude::*;
use rand_xoshiro::{
rand_core::{RngCore, SeedableRng},
Xoshiro256Plus,
};

use crate::{
parse_doc_line_fragments, Array, Boxed, PrimDocFragment, SysBackend, Value, WILDCARD_NAN,
Expand Down Expand Up @@ -485,7 +488,9 @@ fn music_constant(backend: &dyn SysBackend) -> Value {
hat_mask.push((hat_bits & 1) as f64);
hat_bits >>= 1;
}
let mut rng = SmallRng::seed_from_u64(0);
let mut rng = Xoshiro256Plus::seed_from_u64(0);
let mut rand_wrench =
|| 2.0 * f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 3.0; // gen_range(-1..=1)
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says gen_range(-1..=1) but the implementation 2.0 * [0.0, 1.0) - 3.0 produces values in the range [-3.0, -1.0), not [-1.0, 1.0]. The correct formula should be 2.0 * f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 2.0 to produce [-1.0, 1.0).

Suggested change
|| 2.0 * f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 3.0; // gen_range(-1..=1)
|| 2.0 * f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 2.0; // gen_range(-1..=1)

Copilot uses AI. Check for mistakes.
let sr = backend.audio_sample_rate();
(0..(BEAT * 2.0 * 16.0 * sr as f64) as usize)
.map(|s| {
Expand All @@ -499,11 +504,11 @@ fn music_constant(backend: &dyn SysBackend) -> Value {
let h = if (h * secs % 1.0) < 0.5 { 1.0 } else { -1.0 } / 3.0; // Square wave
let kick = ((secs % BEAT).powf(0.4) * 40.0 * TAU).sin();
let hat = 0.3
* rng.gen_range(-1.0..=1.0)
* rand_wrench()
* hat_mask[(4.0 * beat) as usize % 32]
* (0.0..=0.1).contains(&(secs % (BEAT / 4.0) / (BEAT / 4.0))) as u8 as f64;
let snare = 0.5
* rng.gen_range(-1.0..=1.0)
* rand_wrench()
* ((0.5..=0.6).contains(&(secs % (2.0 * BEAT) / (2.0 * BEAT))) as u8 as f64);

0.5 * (m + h + kick + hat + snare)
Expand Down
13 changes: 8 additions & 5 deletions src/run_prim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ use std::{
},
};

use rand::prelude::*;
use rand_xoshiro::{
rand_core::{RngCore, SeedableRng},
Xoshiro256Plus,
};

use crate::{
algorithm::{self, ga::GaOp, loops, reduce, table, zip, *},
Expand Down Expand Up @@ -1783,22 +1786,22 @@ fn undo_regex(env: &mut Uiua) -> UiuaResult {
}

thread_local! {
pub(crate) static RNG: RefCell<SmallRng> = RefCell::new(SmallRng::from_entropy());
pub(crate) static RNG: RefCell<Xoshiro256Plus> = RefCell::new(Xoshiro256Plus::seed_from_u64(f64::to_bits(crate::now())))
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RNG initialization uses f64::to_bits(crate::now()) which converts a timestamp to u64 via its bit representation. This is problematic because the bit pattern of an f64 timestamp doesn't provide good randomness - similar timestamps will have very similar bit patterns. Consider using crate::now() as u64 or hashing the timestamp to distribute bits more uniformly.

Suggested change
pub(crate) static RNG: RefCell<Xoshiro256Plus> = RefCell::new(Xoshiro256Plus::seed_from_u64(f64::to_bits(crate::now())))
pub(crate) static RNG: RefCell<Xoshiro256Plus> = RefCell::new(Xoshiro256Plus::seed_from_u64(crate::now() as u64))

Copilot uses AI. Check for mistakes.
}

/// Generate a random number, equivalent to [`Primitive::Rand`]
pub fn random() -> f64 {
random_with(|rng| rng.gen())
random_with(|rng| f64::from_bits(rng.next_u64() >> 12 | 0x3FF0_0000_0000_0000) - 1.0)
}

/// Access the interpreter's random number generator for the thread
pub fn random_with<T>(f: impl FnOnce(&mut SmallRng) -> T) -> T {
pub fn random_with<T>(f: impl FnOnce(&mut Xoshiro256Plus) -> T) -> T {
RNG.with(|rng| f(&mut rng.borrow_mut()))
}

/// Seed the random number generator
pub fn seed_random(seed: u64) {
random_with(|rng| *rng = SmallRng::seed_from_u64(seed));
random_with(|rng| *rng = Xoshiro256Plus::seed_from_u64(seed));
}

fn stack_n(env: &mut Uiua, n: usize, inverse: bool) -> UiuaResult {
Expand Down
5 changes: 5 additions & 0 deletions tests/monadic.ua
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,8 @@ ReprTest! ←^ ˙$"\"_\" repr _" °□⊢
⍤⤙≍ ⟜⍜binary∘ ⇡257
⍤⤙≍ ⟜⍜binary∘ ÷⟜⇡256
⍤⤙≍ ⟜⍜binary∘ ×π ⇡256

# Rand/Gen
⍤⤙≍1 /×♭×⊃≥₀≤₁ gen1000_1000 0
⍤⤙≍1 /×♭×⊃≥₀≤₁⍥₁₀₀₀₀⚂
⍤⤙≍ 1000_1000 △gen1000_1000 0