Skip to content

Commit 7a36ea3

Browse files
[1/15] Fix repeated side effects in floor/ceiling and runif bounds, reject na.rm= in reductions, and three small crash/diagnostic fixes (#137)
* Evaluate floor()/ceiling() arguments exactly once floor() and ceiling() splice their argument into the emitted Fortran expression three times (once for aint(), twice for the sign adjustment), so an impure argument such as runif(1) was evaluated three times: the result mixed different random draws and the RNG state diverged from R. Extract the hoist-to-temporary pattern already used by matrix() into a shared helper, hoist_unless_name(), and use it in floor()/ceiling() to evaluate non-trivial arguments once. Bare variable names are passed through unchanged, so existing translations without side effects are unaffected. matrix() now uses the shared helper. * Reject named arguments in max/min/sum/prod The reductions handler never inspected argument names, so `sum(x, na.rm = TRUE)` translated `na.rm = TRUE` as an extra data argument, emitting `(sum(x) + .true.)`. Reject named arguments with a clear compile-time error, matching the existing any()/all() guard. * Error cleanly when assigning a value-less expression Binding a new variable to an expression that produces no value (e.g. `y <- if (x) 1 else 2`, where `if` lowers to a statement) crashed with "no applicable method for `@` applied to an object of class NULL". Diagnose it at the assignment site instead, naming the offending expression. * Fix r2size() crash on deferred-mode variables `var@mode != "integer"` fails with "argument is of length zero" when @mode is NULL (a binding whose mode is still being inferred, as in the deferred-mode path in r2f-assign.R). Use !identical() so the intended "size is not an integer" warning path is reached instead. * Make Variable@dims <- NULL reset to scalar The dims setter early-returned on empty input, so resetting a variable to scalar (NULL dims, per the class's own convention) silently kept the stale dims. r2f-assign.R's deferred-mode path assigns `var@dims <- value@value@dims` with a legitimately-NULL RHS and relied on this working. Assign the attribute directly (the same S7 workaround already used by the `r` property) to avoid recursing through the setter. * 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. * Format with air
1 parent 16aa2c5 commit 7a36ea3

21 files changed

Lines changed: 334 additions & 32 deletions

R/classes.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ Variable := new_class(
240240
NULL | class_list,
241241
setter = function(self, value) {
242242
if (!length(value)) {
243+
# reset to scalar (NULL means scalar); assign the attribute
244+
# directly to avoid recursing through this setter
245+
attr(self, "dims") <- NULL
243246
return(self)
244247
}
245248

R/r2f-aab-core.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,26 @@ new_hoist <- function(scope) {
7171
)
7272
}
7373

74+
# Hoist `x` into a temporary variable unless it already renders as a bare
75+
# variable name. Use this whenever the same operand is spliced into generated
76+
# code more than once: Fortran evaluates intrinsic actual arguments before the
77+
# call, so repeating an expression duplicates its side effects (e.g. RNG
78+
# state via runif()).
79+
hoist_unless_name <- function(x, hoist) {
80+
stopifnot(inherits(x, Fortran), inherits(x@value, Variable))
81+
code <- trimws(as.character(x))
82+
if (!is.null(x@value@name) && identical(code, x@value@name)) {
83+
return(x)
84+
}
85+
tmp <- hoist$declare_tmp(
86+
mode = x@value@mode,
87+
dims = x@value@dims,
88+
logical_as_int = logical_as_int(x@value)
89+
)
90+
hoist$emit(glue("{tmp@name} = {x}"))
91+
Fortran(tmp@name, tmp)
92+
}
93+
7494

7595
# --- Scope Helpers ---
7696

R/r2f-assign.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ register_r2f_handler(
175175
if (!existing_binding) {
176176
# The var does not exist -> this is a binding to a new symbol
177177
# Create a fresh Variable carrying only mode/dims and a new name.
178+
if (inherits(value, Fortran) && is.null(value@value)) {
179+
stop(
180+
"cannot assign `",
181+
deparse1(rhs),
182+
"`: expression does not produce a value",
183+
call. = FALSE
184+
)
185+
}
178186
if (!inherits(var, Variable)) {
179187
src <- value@value
180188
var <- Variable(mode = src@mode, dims = src@dims)

R/r2f-constructors.R

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -157,24 +157,10 @@ r2f_handlers[["matrix"]] <- function(args, scope = NULL, ..., hoist = NULL) {
157157

158158
rows <- dims[[1L]]
159159
cols <- dims[[2L]]
160-
source <- glue("{src}")
161160

162161
# Avoid double-evaluating non-trivial expressions when used in both the
163-
# `source` and `pad` args. In Fortran, intrinsic actual args are evaluated
164-
# before the call, so repeating the expression can duplicate side effects
165-
# (e.g. RNG state via runif()).
166-
if (
167-
is.null(src@value@name) ||
168-
!identical(trimws(source), src@value@name)
169-
) {
170-
tmp <- hoist$declare_tmp(
171-
mode = src@value@mode,
172-
dims = src@value@dims,
173-
logical_as_int = logical_as_int(src@value)
174-
)
175-
hoist$emit(glue("{tmp@name} = {src}"))
176-
source <- tmp@name
177-
}
162+
# `source` and `pad` args.
163+
source <- glue("{hoist_unless_name(src, hoist)}")
178164
Fortran(
179165
glue(
180166
"reshape({source}, [{bind_dim_int(rows)}, {bind_dim_int(cols)}], pad = {source})"

R/r2f-math.R

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@ register_unary_intrinsic(
4242
expr_fun = function(arg, intrinsic) glue("{intrinsic}({arg})")
4343
)
4444

45-
r2f_handlers[["floor"]] <- function(args, scope, ...) {
45+
r2f_handlers[["floor"]] <- function(args, scope, ..., hoist = NULL) {
4646
stopifnot(length(args) == 1L)
47-
arg <- r2f(args[[1L]], scope, ...)
47+
arg <- r2f(args[[1L]], scope, ..., hoist = hoist)
4848

49+
# `arg` is spliced into the emitted expression three times below; hoist
50+
# non-trivial expressions so side effects (e.g. RNG state) happen once.
51+
arg <- hoist_unless_name(arg, hoist)
4952
arg <- maybe_cast_double(arg)
5053
if (!identical(arg@value@mode, "double")) {
5154
stop("floor() only implemented for logical, integer, and double")
@@ -63,10 +66,13 @@ r2f_handlers[["floor"]] <- function(args, scope, ...) {
6366
)
6467
}
6568

66-
r2f_handlers[["ceiling"]] <- function(args, scope, ...) {
69+
r2f_handlers[["ceiling"]] <- function(args, scope, ..., hoist = NULL) {
6770
stopifnot(length(args) == 1L)
68-
arg <- r2f(args[[1L]], scope, ...)
71+
arg <- r2f(args[[1L]], scope, ..., hoist = hoist)
6972

73+
# `arg` is spliced into the emitted expression three times below; hoist
74+
# non-trivial expressions so side effects (e.g. RNG state) happen once.
75+
arg <- hoist_unless_name(arg, hoist)
7076
arg <- maybe_cast_double(arg)
7177
if (!identical(arg@value@mode, "double")) {
7278
stop("ceiling() only implemented for logical, integer, and double")

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

R/r2f-reductions.R

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ register_r2f_handler(
1313
scope,
1414
...
1515
) {
16+
# Named arguments like `na.rm` would otherwise be treated as data
17+
# arguments (e.g. `sum(x, na.rm = TRUE)` -> `(sum(x) + .true.)`).
18+
arg_names <- names(args) %||% character()
19+
if (length(arg_names) && any(nzchar(arg_names))) {
20+
stop(
21+
"max()/min()/sum()/prod() do not support named arguments (e.g. `na.rm`)",
22+
call. = FALSE
23+
)
24+
}
25+
1626
intrinsic <- switch(
1727
last(list(...)$calls),
1828
max = "maxval",

R/sizes.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ r2size <- function(r, scope) {
168168
if (!inherits(var, Variable)) {
169169
stop("could not resolve size: ", as.character(r))
170170
}
171-
if (var@mode != "integer" || !passes_as_scalar(var)) {
171+
# !identical(): @mode can be NULL (deferred-mode binding)
172+
if (!identical(var@mode, "integer") || !passes_as_scalar(var)) {
172173
warning("size is not an integer:", as.character(r))
173174
}
174175
if (var@is_arg && !var@modified) {

tests/testthat/_snaps/example-convolve.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
ab <- double(length(a) + length(b) - 1)
1111
for (i in seq_along(a)) {
1212
for (j in seq_along(b)) {
13-
ab[i + j - 1] = ab[i + j - 1] + a[i] * b[j]
13+
ab[i + j - 1] <- ab[i + j - 1] + a[i] * b[j]
1414
}
1515
}
1616
ab
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# floor() hoists non-name arguments to a temporary
2+
3+
Code
4+
fn
5+
Output
6+
function() {
7+
out <- floor(runif(1L) * 10)
8+
out
9+
}
10+
<environment: 0x0>
11+
Code
12+
cat(fsub)
13+
Output
14+
subroutine fn(out) bind(c)
15+
use iso_c_binding, only: c_double
16+
implicit none
17+
18+
! manifest start
19+
! args
20+
real(c_double), intent(out) :: out
21+
! manifest end
22+
23+
interface
24+
function unif_rand() bind(c, name = "unif_rand") result(u)
25+
use iso_c_binding, only: c_double
26+
real(c_double) :: u
27+
end function unif_rand
28+
end interface
29+
30+
block
31+
real(c_double) :: btmp1_
32+
33+
btmp1_ = (unif_rand() * 10.0_c_double)
34+
out = (aint(btmp1_) - merge(1.0_c_double, 0.0_c_double, (btmp1_ < aint(btmp1_))))
35+
end block
36+
end subroutine
37+
Code
38+
cat(cwrapper)
39+
Output
40+
#define R_NO_REMAP
41+
#include <R.h>
42+
#include <Rinternals.h>
43+
#include <R_ext/Random.h>
44+
45+
46+
extern void fn(double* const out__);
47+
48+
SEXP fn_(SEXP _args) {
49+
50+
const R_xlen_t out__len_ = (1);
51+
SEXP out = PROTECT(Rf_allocVector(REALSXP, out__len_));
52+
double* out__ = REAL(out);
53+
54+
GetRNGstate();
55+
fn(out__);
56+
PutRNGstate();
57+
58+
UNPROTECT(1);
59+
return out;
60+
}
61+

0 commit comments

Comments
 (0)