Port random to rust

This commit is contained in:
Fabian Boehm 2023-02-18 22:06:05 +01:00
parent bc7c29d597
commit 4fd1458d85
12 changed files with 256 additions and 181 deletions

View File

@ -107,7 +107,7 @@ set(FISH_BUILTIN_SRCS
src/builtins/eval.cpp src/builtins/fg.cpp
src/builtins/function.cpp src/builtins/functions.cpp src/builtins/history.cpp
src/builtins/jobs.cpp src/builtins/math.cpp src/builtins/printf.cpp src/builtins/path.cpp
src/builtins/pwd.cpp src/builtins/random.cpp src/builtins/read.cpp
src/builtins/pwd.cpp src/builtins/read.cpp
src/builtins/realpath.cpp src/builtins/set.cpp
src/builtins/set_color.cpp src/builtins/source.cpp src/builtins/status.cpp
src/builtins/string.cpp src/builtins/test.cpp src/builtins/type.cpp src/builtins/ulimit.cpp

37
fish-rust/Cargo.lock generated
View File

@ -358,6 +358,7 @@ dependencies = [
"nix",
"num-traits",
"once_cell",
"rand",
"unixstring",
"widestring",
"widestring-suffix",
@ -658,6 +659,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettyplease"
version = "0.1.23"
@ -710,6 +717,36 @@ dependencies = [
"proc-macro2",
]
[[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",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"

View File

@ -15,6 +15,7 @@ libc = "0.2.137"
nix = { version = "0.25.0", default-features = false, features = [] }
num-traits = "0.2.15"
once_cell = "1.17.0"
rand = { version = "0.8.5", features = ["small_rng"] }
unixstring = "0.2.7"
widestring = "1.0.2"

View File

@ -2,6 +2,7 @@ pub mod shared;
pub mod echo;
pub mod emit;
mod exit;
pub mod random;
pub mod r#return;
pub mod wait;
mod exit;

View File

@ -0,0 +1,188 @@
use libc::c_int;
use crate::builtins::shared::{
builtin_missing_argument, builtin_print_help, builtin_unknown_option, io_streams_t,
STATUS_CMD_OK, STATUS_INVALID_ARGS,
};
use crate::ffi::parser_t;
use crate::wchar::{widestrs, wstr};
use crate::wgetopt::{wgetopter_t, wopt, woption, woption_argument_t};
use crate::wutil::{self, fish_wcstoi_radix_all, format::printf::sprintf, wgettext_fmt};
use num_traits::PrimInt;
use once_cell::sync::Lazy;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::sync::Mutex;
static seeded_engine: Lazy<Mutex<SmallRng>> = Lazy::new(|| Mutex::new(SmallRng::from_entropy()));
#[widestrs]
pub fn random(
parser: &mut parser_t,
streams: &mut io_streams_t,
argv: &mut [&wstr],
) -> Option<c_int> {
let cmd = argv[0];
let argc = argv.len();
let print_hints = false;
const shortopts: &wstr = "+:h"L;
const longopts: &[woption] = &[wopt("help"L, woption_argument_t::no_argument, 'h')];
let mut w = wgetopter_t::new(shortopts, longopts, argv);
while let Some(c) = w.wgetopt_long() {
match c {
'h' => {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
':' => {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
'?' => {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1], print_hints);
return STATUS_INVALID_ARGS;
}
_ => {
panic!("unexpected retval from wgeopter.next()");
}
}
}
let mut engine = seeded_engine.lock().unwrap();
let mut start = 0;
let mut end = 32767;
let mut step = 1;
let arg_count = argc - w.woptind;
let i = w.woptind;
if arg_count >= 1 && argv[i] == "choice" {
if arg_count == 1 {
streams
.err
.append(wgettext_fmt!("%ls: nothing to choose from\n", cmd,));
return STATUS_INVALID_ARGS;
}
let rand = engine.gen_range(0..arg_count - 1);
streams.out.append(sprintf!("%ls\n"L, argv[i + 1 + rand]));
return STATUS_CMD_OK;
}
fn parse<T: PrimInt>(
streams: &mut io_streams_t,
cmd: &wstr,
num: &wstr,
) -> Result<T, wutil::Error> {
let res = fish_wcstoi_radix_all(num.chars(), None, true);
if res.is_err() {
streams
.err
.append(wgettext_fmt!("%ls: %ls: invalid integer\n", cmd, num,));
}
return res;
}
match arg_count {
0 => {
// Keep the defaults
}
1 => {
// Seed the engine persistently
let num = parse::<i64>(streams, cmd, argv[i]);
match num {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => *engine = SmallRng::seed_from_u64(x as u64),
}
return STATUS_CMD_OK;
}
2 => {
// start is first, end is second
match parse::<i64>(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
match parse::<i64>(streams, cmd, argv[i + 1]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => end = x,
}
}
3 => {
// start, step, end
match parse::<i64>(streams, cmd, argv[i]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => start = x,
}
// start, step, end
match parse::<u64>(streams, cmd, argv[i + 1]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(0) => {
streams
.err
.append(wgettext_fmt!("%ls: STEP must be a positive integer\n", cmd,));
return STATUS_INVALID_ARGS;
}
Ok(x) => step = x,
}
match parse::<i64>(streams, cmd, argv[i + 2]) {
Err(_) => return STATUS_INVALID_ARGS,
Ok(x) => end = x,
}
}
_ => {
streams
.err
.append(wgettext_fmt!("%ls: too many arguments\n", cmd,));
return Some(1);
}
}
if end <= start {
streams
.err
.append(wgettext_fmt!("%ls: END must be greater than START\n", cmd,));
return STATUS_INVALID_ARGS;
}
// Possibilities can be abs(i64::MIN) + i64::MAX,
// so we do this as i128
let possibilities = (end as i128 - start as i128) / (step as i128);
if possibilities == 0 {
streams.err.append(wgettext_fmt!(
"%ls: range contains only one possible value\n",
cmd,
));
return STATUS_INVALID_ARGS;
}
let rand = engine.gen_range(0..=possibilities);
let result = start as i128 + rand as i128 * step as i128;
// We do our math as i128,
// and then we check if it fits in 64 bit - signed or unsigned!
match i64::try_from(result) {
Ok(x) => {
streams.out.append(sprintf!("%d\n"L, x));
return STATUS_CMD_OK;
},
Err(_) => {
match u64::try_from(result) {
Ok(x) => {
streams.out.append(sprintf!("%d\n"L, x));
return STATUS_CMD_OK;
},
Err(_) => {
streams.err.append(wgettext_fmt!(
"%ls: range contains only one possible value\n",
cmd,
));
return STATUS_INVALID_ARGS;
},
}
},
}
}

View File

@ -118,6 +118,7 @@ pub fn run_builtin(
RustBuiltin::Echo => super::echo::echo(parser, streams, args),
RustBuiltin::Emit => super::emit::emit(parser, streams, args),
RustBuiltin::Exit => super::exit::exit(parser, streams, args),
RustBuiltin::Random => super::random::random(parser, streams, args),
RustBuiltin::Return => super::r#return::r#return(parser, streams, args),
RustBuiltin::Wait => wait::wait(parser, streams, args),
}

View File

@ -106,11 +106,19 @@ where
negative = false;
}
let consumed_all = chars.peek() == None;
Ok(ParseResult { result, negative, consumed_all })
Ok(ParseResult {
result,
negative,
consumed_all,
})
}
/// Parse some iterator over Chars into some Integer type, optionally with a radix.
fn fish_wcstoi_impl<Int, Chars>(src: Chars, mradix: Option<u32>, consume_all: bool) -> Result<Int, Error>
fn fish_wcstoi_impl<Int, Chars>(
src: Chars,
mradix: Option<u32>,
consume_all: bool,
) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
Int: PrimInt,
@ -120,7 +128,10 @@ where
let signed = Int::min_value() < Int::zero();
let ParseResult {
result, negative, consumed_all, ..
result,
negative,
consumed_all,
..
} = fish_parse_radix(src, mradix)?;
if !signed && negative {
@ -169,7 +180,11 @@ where
fish_wcstoi_impl(src, Some(radix), false)
}
pub fn fish_wcstoi_radix_all<Int, Chars>(src: Chars, radix: Option<u32>, consume_all: bool) -> Result<Int, Error>
pub fn fish_wcstoi_radix_all<Int, Chars>(
src: Chars,
radix: Option<u32>,
consume_all: bool,
) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
Int: PrimInt,

View File

@ -50,7 +50,6 @@
#include "builtins/path.h"
#include "builtins/printf.h"
#include "builtins/pwd.h"
#include "builtins/random.h"
#include "builtins/read.h"
#include "builtins/realpath.h"
#include "builtins/set.h"
@ -401,7 +400,7 @@ static constexpr builtin_data_t builtin_datas[] = {
{L"path", &builtin_path, N_(L"Handle paths")},
{L"printf", &builtin_printf, N_(L"Prints formatted text")},
{L"pwd", &builtin_pwd, N_(L"Print the working directory")},
{L"random", &builtin_random, N_(L"Generate random number")},
{L"random", &implemented_in_rust, N_(L"Generate random number")},
{L"read", &builtin_read, N_(L"Read a line of input into variables")},
{L"realpath", &builtin_realpath, N_(L"Show absolute path sans symlinks")},
{L"return", &implemented_in_rust, N_(L"Stop the currently evaluated function")},
@ -534,6 +533,9 @@ static maybe_t<RustBuiltin> try_get_rust_builtin(const wcstring &cmd) {
if (cmd == L"exit") {
return RustBuiltin::Exit;
}
if (cmd == L"random") {
return RustBuiltin::Random;
}
if (cmd == L"wait") {
return RustBuiltin::Wait;
}

View File

@ -112,7 +112,8 @@ enum RustBuiltin : int32_t {
Echo,
Emit,
Exit,
Wait,
Random,
Return,
Wait,
};
#endif

View File

@ -1,160 +0,0 @@
// Implementation of the random builtin.
#include "config.h" // IWYU pragma: keep
#include "random.h"
#include <cerrno>
#include <cstdint>
#include <cwchar>
#include <random>
#include "../builtin.h"
#include "../common.h"
#include "../fallback.h" // IWYU pragma: keep
#include "../io.h"
#include "../maybe.h"
#include "../wutil.h" // IWYU pragma: keep
/// \return a random-seeded engine.
static std::minstd_rand get_seeded_engine() {
std::minstd_rand engine;
// seed engine with 2*32 bits of random data
// for the 64 bits of internal state of minstd_rand
std::random_device rd;
std::seed_seq seed{rd(), rd()};
engine.seed(seed);
return engine;
}
/// The random builtin generates random numbers.
maybe_t<int> builtin_random(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
const wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
help_only_cmd_opts_t opts;
int optind;
int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
if (opts.print_help) {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
// We have a single engine which we lazily seed. Lock it here.
static owning_lock<std::minstd_rand> s_engine{get_seeded_engine()};
auto engine_lock = s_engine.acquire();
std::minstd_rand &engine = *engine_lock;
int arg_count = argc - optind;
long long start, end;
unsigned long long step;
bool choice = false;
if (arg_count >= 1 && !std::wcscmp(argv[optind], L"choice")) {
if (arg_count == 1) {
streams.err.append_format(L"%ls: nothing to choose from\n", cmd);
return STATUS_INVALID_ARGS;
}
choice = true;
start = 1;
step = 1;
end = arg_count - 1;
} else {
bool parse_error = false;
auto parse_ll = [&](const wchar_t *str) {
long long ll = fish_wcstoll(str);
if (errno) {
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, str);
parse_error = true;
}
return ll;
};
auto parse_ull = [&](const wchar_t *str) {
unsigned long long ull = fish_wcstoull(str);
if (errno) {
streams.err.append_format(BUILTIN_ERR_NOT_NUMBER, cmd, str);
parse_error = true;
}
return ull;
};
if (arg_count == 0) {
start = 0;
end = 32767;
step = 1;
} else if (arg_count == 1) {
long long seed = parse_ll(argv[optind]);
if (parse_error) return STATUS_INVALID_ARGS;
engine.seed(static_cast<uint32_t>(seed));
return STATUS_CMD_OK;
} else if (arg_count == 2) {
start = parse_ll(argv[optind]);
step = 1;
end = parse_ll(argv[optind + 1]);
} else if (arg_count == 3) {
start = parse_ll(argv[optind]);
step = parse_ull(argv[optind + 1]);
end = parse_ll(argv[optind + 2]);
} else {
streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
return STATUS_INVALID_ARGS;
}
if (parse_error) {
return STATUS_INVALID_ARGS;
} else if (start >= end) {
streams.err.append_format(L"%ls: END must be greater than START\n", cmd);
return STATUS_INVALID_ARGS;
} else if (step == 0) {
streams.err.append_format(L"%ls: STEP must be a positive integer\n", cmd);
return STATUS_INVALID_ARGS;
}
}
// only for negative argument
auto safe_abs = [](long long ll) -> unsigned long long {
return -static_cast<unsigned long long>(ll);
};
long long real_end;
if (start >= 0 || end < 0) {
// 0 <= start <= end
long long diff = end - start;
// 0 <= diff <= LL_MAX
real_end = start + static_cast<long long>(diff / step);
} else {
// start < 0 <= end
unsigned long long abs_start = safe_abs(start);
unsigned long long diff = (end + abs_start);
real_end = diff / step - abs_start;
}
if (!choice && start == real_end) {
streams.err.append_format(L"%ls: range contains only one possible value\n", cmd);
return STATUS_INVALID_ARGS;
}
std::uniform_int_distribution<long long> dist(start, real_end);
long long random = dist(engine);
long long result;
if (start >= 0) {
// 0 <= start <= random <= end
long long diff = random - start;
// 0 < step * diff <= end - start <= LL_MAX
result = start + static_cast<long long>(diff * step);
} else if (random < 0) {
// start <= random < 0
long long diff = random - start;
result = diff * step - safe_abs(start);
} else {
// start < 0 <= random
unsigned long long abs_start = safe_abs(start);
unsigned long long diff = (random + abs_start);
result = diff * step - abs_start;
}
if (choice) {
streams.out.append_format(L"%ls\n", argv[optind + result]);
} else {
streams.out.append_format(L"%lld\n", result);
}
return STATUS_CMD_OK;
}

View File

@ -1,11 +0,0 @@
// Prototypes for executing builtin_random function.
#ifndef FISH_BUILTIN_RANDOM_H
#define FISH_BUILTIN_RANDOM_H
#include "../maybe.h"
class parser_t;
struct io_streams_t;
maybe_t<int> builtin_random(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
#endif

View File

@ -40,7 +40,7 @@ random choic a b c
#CHECKERR: random: too many arguments
function check_boundaries
if not test $argv[1] -ge $argv[2] -a $argv[1] -le $argv[3]
if not test "$argv[1]" -ge "$argv[2]" -a "$argv[1]" -le "$argv[3]"
printf "Unexpected: %s <= %s <= %s not verified\n" $argv[2] $argv[1] $argv[3] >&2
return 1
end