Skip to content

Commit b3f5894

Browse files
committed
Evaluate runif() bounds exactly once
Same defect class as the floor()/ceiling() fix: `min` is spliced twice into the emitted expression, and for array results the implied-do re-evaluates spliced bounds once per element, so an impure bound such as `runif(2L, runif(1L), 10)` drew a fresh `min` value repeatedly where R evaluates it once. Hoist non-trivial bounds via hoist_unless_name(); bare names and literal bounds are unaffected.
1 parent b730736 commit b3f5894

3 files changed

Lines changed: 100 additions & 3 deletions

File tree

R/r2f-random.R

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,22 @@ r2f_handlers[["runif"]] <- function(args, scope, ..., hoist = NULL) {
1414
default_min <- identical(min, 0) || identical(min, 0L)
1515
default_max <- identical(max, 1) || identical(max, 1L)
1616

17+
# R evaluates runif() bounds exactly once, but `min` is spliced twice below
18+
# and the implied-do re-evaluates the whole expression per element; hoist
19+
# non-trivial bounds (e.g. an impure runif(1)) so they are evaluated once.
20+
bound <- function(r_arg) {
21+
b <- r2f(r_arg, scope, ..., hoist = hoist)
22+
if (is.atomic(r_arg)) b else hoist_unless_name(b, hoist)
23+
}
24+
1725
if (default_min && default_max) {
1826
get1rand <- "unif_rand()"
1927
} else if (default_min) {
20-
max <- r2f(max, scope, ..., hoist = hoist)
28+
max <- bound(max)
2129
get1rand <- glue("unif_rand() * {max}")
2230
} else {
23-
max <- r2f(max, scope, ..., hoist = hoist)
24-
min <- r2f(min, scope, ..., hoist = hoist)
31+
min <- bound(min)
32+
max <- bound(max)
2533
get1rand <- glue("({min} + (unif_rand() * ({max} - {min})))")
2634
}
2735

tests/testthat/_snaps/runif.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,3 +566,67 @@
566566
return out_;
567567
}
568568

569+
# impure runif() bounds are evaluated exactly once
570+
571+
Code
572+
fn
573+
Output
574+
function() {
575+
out <- runif(2L, runif(1L), 10)
576+
out
577+
}
578+
<environment: 0x0>
579+
Code
580+
cat(fsub)
581+
Output
582+
subroutine fn(out) bind(c)
583+
use iso_c_binding, only: c_double, c_int
584+
implicit none
585+
586+
! manifest start
587+
! args
588+
real(c_double), intent(out) :: out(2)
589+
590+
! locals
591+
integer(c_int) :: tmp1_
592+
! manifest end
593+
594+
interface
595+
function unif_rand() bind(c, name = "unif_rand") result(u)
596+
use iso_c_binding, only: c_double
597+
real(c_double) :: u
598+
end function unif_rand
599+
end interface
600+
601+
block
602+
real(c_double) :: btmp1_
603+
604+
btmp1_ = unif_rand()
605+
out = [((btmp1_ + (unif_rand() * (10.0_c_double - btmp1_))), tmp1_=1, 2)]
606+
end block
607+
end subroutine
608+
Code
609+
cat(cwrapper)
610+
Output
611+
#define R_NO_REMAP
612+
#include <R.h>
613+
#include <Rinternals.h>
614+
#include <R_ext/Random.h>
615+
616+
617+
extern void fn(double* const out__);
618+
619+
SEXP fn_(SEXP _args) {
620+
621+
const R_xlen_t out__len_ = 2;
622+
SEXP out = PROTECT(Rf_allocVector(REALSXP, out__len_));
623+
double* out__ = REAL(out);
624+
625+
GetRNGstate();
626+
fn(out__);
627+
PutRNGstate();
628+
629+
UNPROTECT(1);
630+
return out;
631+
}
632+

tests/testthat/test-runif.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,28 @@ test_that("runif with min/max", {
117117
set_seed_and_call(qfn, 20)
118118
)
119119
})
120+
121+
test_that("impure runif() bounds are evaluated exactly once", {
122+
# `min` is spliced twice into the emitted expression, and the implied-do
123+
# for array results would re-evaluate spliced bounds per element; R
124+
# evaluates bounds once per call.
125+
fn <- function() {
126+
out <- runif(2L, runif(1L), 10)
127+
out
128+
}
129+
expect_translation_snapshots(fn)
130+
qfn <- quick(fn)
131+
132+
expect_identical(
133+
set_seed_and_call(fn),
134+
set_seed_and_call(qfn)
135+
)
136+
137+
set.seed(1)
138+
qfn()
139+
q_next <- runif(1L)
140+
set.seed(1)
141+
fn()
142+
r_next <- runif(1L)
143+
expect_identical(q_next, r_next)
144+
})

0 commit comments

Comments
 (0)