Skip to content

Commit 1e7d819

Browse files
committed
Fix hoisting in unbraced for-loop bodies
Braced loop bodies give each statement its own hoist target, but a single-expression for-loop body inherited the target of the enclosing statement. Body-local setup could therefore be emitted before the loop, where it executed only once and could reference an uninitialized loop variable. Give both index- and value-iteration bodies a fresh hoist target. Iterable setup remains outside the loop, while guards and temporaries required by the body are emitted inside it.
1 parent 68874ec commit 1e7d819

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

R/r2f-control-flow.R

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ r2f_handlers[["while"]] <- function(args, scope, ...) {
7777
}
7878

7979
# ---- for ----
80-
r2f_handlers[["for"]] <- function(args, scope, ...) {
80+
r2f_handlers[["for"]] <- function(args, scope, ..., hoist = NULL) {
8181
.[var, iterable, body] <- args
8282
stopifnot(is.symbol(var))
8383
var <- as.character(var)
@@ -174,7 +174,10 @@ r2f_handlers[["for"]] <- function(args, scope, ...) {
174174
previous_openmp <- enter_openmp_scope(scope)
175175
on.exit(exit_openmp_scope(scope, previous_openmp), add = TRUE)
176176
}
177-
body <- r2f(body, scope, ...)
177+
# The body is a distinct execution region and needs its own hoist target.
178+
# Otherwise a single-expression body reuses the enclosing statement's
179+
# target and emits loop-dependent setup before the loop.
180+
body <- r2f(body, scope, ..., hoist = NULL)
178181
check_pending_parallel_consumed(scope)
179182
loop_stmts <- str_flatten_lines(glue("{var_name} = {element_expr}"), body)
180183

@@ -214,12 +217,14 @@ r2f_handlers[["for"]] <- function(args, scope, ...) {
214217
}
215218
scope[[var]] <- loop_var
216219

217-
iterable <- r2f_for_iterable(iterable, scope, ...)
220+
iterable <- r2f_for_iterable(iterable, scope, ..., hoist = hoist)
218221
if (!is.null(parallel)) {
219222
previous_openmp <- enter_openmp_scope(scope)
220223
on.exit(exit_openmp_scope(scope, previous_openmp), add = TRUE)
221224
}
222-
body <- r2f(body, scope, ...)
225+
# See the value-iteration path above: body-local setup must run inside the
226+
# loop even when the R body is not wrapped in braces.
227+
body <- r2f(body, scope, ..., hoist = NULL)
223228
check_pending_parallel_consumed(scope)
224229

225230
directives <- openmp_directives(parallel)

tests/testthat/test-for-iterables.R

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,58 @@ test_that("for() supports rev() for value iteration", {
288288
expect_quick_identical(rev_values, 1:3, c(1L, 2L, 3L, 4L))
289289
expect_quick_identical(rev_matrix_values, matrix(1:6, nrow = 2))
290290
})
291+
292+
test_that("unbraced for() bodies keep hoisted setup inside index loops", {
293+
rolling_sum <- function(x, weights) {
294+
declare(type(x = double(NA)), type(weights = double(NA)))
295+
out <- double(length(x) - length(weights) + 1L)
296+
n <- length(weights)
297+
# fmt: skip
298+
for (i in seq_along(out))
299+
out[i] <- sum(x[i:(i + n - 1L)] * weights)
300+
out
301+
}
302+
303+
seq_lengths <- function(k) {
304+
declare(type(k = integer(1)))
305+
out <- 0L
306+
# fmt: skip
307+
for (i in 1:3)
308+
out <- out + length(seq(i, i + 3L, by = k))
309+
out
310+
}
311+
312+
expect_quick_identical(
313+
rolling_sum,
314+
list(as.double(1:8), c(0.25, 0.75))
315+
)
316+
expect_quick_identical(seq_lengths, 1L, 2L)
317+
318+
qseq_lengths := quick(seq_lengths)
319+
expect_error(
320+
qseq_lengths(0L),
321+
"invalid '(to - from)/by'",
322+
fixed = TRUE
323+
)
324+
expect_error(
325+
qseq_lengths(-1L),
326+
"wrong sign in 'by' argument",
327+
fixed = TRUE
328+
)
329+
})
330+
331+
test_that("unbraced for() bodies keep hoisted setup inside value loops", {
332+
sum_floors <- function(starts) {
333+
declare(type(starts = integer(NA)))
334+
out <- 0
335+
# fmt: skip
336+
for (i in starts)
337+
out <- out + floor(i + 0.5)
338+
out
339+
}
340+
341+
expect_quick_identical(
342+
sum_floors,
343+
c(1L, 3L, 5L)
344+
)
345+
})

0 commit comments

Comments
 (0)