Skip to content

Commit b92efe7

Browse files
committed
Give single-statement while/repeat bodies their own hoist target
while and repeat forwarded the enclosing statement's hoist into the loop body. A `{` body is unaffected (each statement gets its own hoist), but a single-statement body's hoisted code -- BLAS calls, temporaries, runtime guards -- was emitted once, before the loop: while (m[1, 1] < 8) m <- m %*% m compiled the dgemm call ahead of `do while` with `m = btmp1_` as the loop body, an infinite loop (or stale result) where R terminates. Both bodies now compile with their own per-statement hoist, exactly as `{` bodies, the `if` handler's branches and (since #141) the `for` handler already do. Pre-existing bug (predates this branch), surfaced by review (fable-final-review.md #1); the condition-side fix in the previous commit made the pattern obvious.
1 parent 82707b0 commit b92efe7

3 files changed

Lines changed: 186 additions & 3 deletions

File tree

R/r2f-control-flow.R

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ r2f_handlers[["if"]] <- function(args, scope, ..., hoist = NULL) {
3838
# TODO: return
3939

4040
# ---- repeat ----
41-
r2f_handlers[["repeat"]] <- function(args, scope, ...) {
41+
r2f_handlers[["repeat"]] <- function(args, scope, ..., hoist = NULL) {
4242
stopifnot(length(args) == 1L)
43-
body <- r2f(args[[1]], scope, ...)
43+
# The body gets its own hoist target: forwarding the enclosing
44+
# statement's hoist would emit a single-statement body's hoisted code
45+
# (BLAS calls, temporaries, guards) once, before the loop, instead of
46+
# per iteration. (`{` bodies already isolate each statement.)
47+
body <- r2f(args[[1]], scope, ..., hoist = NULL)
4448
check_pending_parallel_consumed(scope)
4549
Fortran(glue(
4650
"do
@@ -72,7 +76,11 @@ r2f_handlers[["while"]] <- function(args, scope, ..., hoist = NULL) {
7276
# present, lower to an explicit exit check at the top of the loop body.
7377
cond_hoist <- new_hoist(scope)
7478
cond <- r2f(args[[1]], scope, ..., hoist = cond_hoist)
75-
body <- r2f(args[[2]], scope, ..., hoist = hoist)
79+
# The body gets its own hoist target for the same reason: forwarding the
80+
# enclosing statement's hoist would emit a single-statement body's
81+
# hoisted code (BLAS calls, temporaries, guards) once, before the loop.
82+
# (`{` bodies already isolate each statement.)
83+
body <- r2f(args[[2]], scope, ..., hoist = NULL)
7684
check_pending_parallel_consumed(scope)
7785
exit_check <- glue("if (.not. ({cond})) exit")
7886
cond_code <- cond_hoist$render(exit_check)

tests/testthat/_snaps/loops.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,153 @@
461461
return out_;
462462
}
463463

464+
# single-statement while/repeat bodies re-run their hoisted statements
465+
466+
Code
467+
fn
468+
Output
469+
function(m) {
470+
declare(type(m = double(2, 2)))
471+
while (m[1, 1] < 100) m <- m %*% m
472+
m
473+
}
474+
<environment: 0x0>
475+
Code
476+
cat(fsub)
477+
Output
478+
subroutine fn(m) bind(c)
479+
use iso_c_binding, only: c_double, c_int
480+
implicit none
481+
482+
! manifest start
483+
! args
484+
real(c_double), intent(in out) :: m(2, 2)
485+
! manifest end
486+
487+
488+
do while ((m(1_c_int, 1_c_int) < 100.0_c_double))
489+
block
490+
real(c_double) :: btmp1_(2, 2)
491+
492+
call dgemm('N','N', int(2, kind=c_int), int(2, kind=c_int), int(2, kind=c_int), 1.0_c_double, m, int(2, kind=c_int), m, int(2,&
493+
& kind=c_int), 0.0_c_double, btmp1_, int(2, kind=c_int))
494+
m = btmp1_
495+
end block
496+
end do
497+
end subroutine
498+
Code
499+
cat(cwrapper)
500+
Output
501+
#define R_NO_REMAP
502+
#include <R.h>
503+
#include <Rinternals.h>
504+
505+
506+
extern void fn(double* const m__);
507+
508+
SEXP fn_(SEXP _args) {
509+
// m
510+
_args = CDR(_args);
511+
SEXP m = CAR(_args);
512+
if (TYPEOF(m) != REALSXP) {
513+
Rf_error("typeof(m) must be 'double', not '%s'", Rf_type2char(TYPEOF(m)));
514+
}
515+
m = Rf_duplicate(m);
516+
SETCAR(_args, m);
517+
double* const m__ = REAL(m);
518+
const int* const m__dim_ = ({
519+
SEXP dim_ = Rf_getAttrib(m, R_DimSymbol);
520+
if (Rf_length(dim_) != 2) Rf_error(
521+
"m must be a 2D-array, but length(dim(m)) is %i",
522+
(int) Rf_length(dim_));
523+
INTEGER(dim_);});
524+
const int m__dim_1_ = m__dim_[0];
525+
const int m__dim_2_ = m__dim_[1];
526+
527+
if (m__dim_1_ != 2)
528+
Rf_error("dim(m)[1] must be 2, not %0.f",
529+
(double)m__dim_1_);
530+
if (m__dim_2_ != 2)
531+
Rf_error("dim(m)[2] must be 2, not %0.f",
532+
(double)m__dim_2_);
533+
534+
fn(m__);
535+
536+
return m;
537+
}
538+
539+
---
540+
541+
Code
542+
fn
543+
Output
544+
function(m) {
545+
declare(type(m = double(2, 2)))
546+
repeat m <- m %*% m
547+
m
548+
}
549+
<environment: 0x0>
550+
Code
551+
cat(fsub)
552+
Output
553+
subroutine fn(m) bind(c)
554+
use iso_c_binding, only: c_double, c_int
555+
implicit none
556+
557+
! manifest start
558+
! args
559+
real(c_double), intent(in out) :: m(2, 2)
560+
! manifest end
561+
562+
563+
do
564+
block
565+
real(c_double) :: btmp1_(2, 2)
566+
567+
call dgemm('N','N', int(2, kind=c_int), int(2, kind=c_int), int(2, kind=c_int), 1.0_c_double, m, int(2, kind=c_int), m, int(2,&
568+
& kind=c_int), 0.0_c_double, btmp1_, int(2, kind=c_int))
569+
m = btmp1_
570+
end block
571+
end do
572+
end subroutine
573+
Code
574+
cat(cwrapper)
575+
Output
576+
#define R_NO_REMAP
577+
#include <R.h>
578+
#include <Rinternals.h>
579+
580+
581+
extern void fn(double* const m__);
582+
583+
SEXP fn_(SEXP _args) {
584+
// m
585+
_args = CDR(_args);
586+
SEXP m = CAR(_args);
587+
if (TYPEOF(m) != REALSXP) {
588+
Rf_error("typeof(m) must be 'double', not '%s'", Rf_type2char(TYPEOF(m)));
589+
}
590+
m = Rf_duplicate(m);
591+
SETCAR(_args, m);
592+
double* const m__ = REAL(m);
593+
const int* const m__dim_ = ({
594+
SEXP dim_ = Rf_getAttrib(m, R_DimSymbol);
595+
if (Rf_length(dim_) != 2) Rf_error(
596+
"m must be a 2D-array, but length(dim(m)) is %i",
597+
(int) Rf_length(dim_));
598+
INTEGER(dim_);});
599+
const int m__dim_1_ = m__dim_[0];
600+
const int m__dim_2_ = m__dim_[1];
601+
602+
if (m__dim_1_ != 2)
603+
Rf_error("dim(m)[1] must be 2, not %0.f",
604+
(double)m__dim_1_);
605+
if (m__dim_2_ != 2)
606+
Rf_error("dim(m)[2] must be 2, not %0.f",
607+
(double)m__dim_2_);
608+
609+
fn(m__);
610+
611+
return m;
612+
}
613+

tests/testthat/test-loops.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,28 @@ test_that("expr return value", {
103103
expect_translation_snapshots(fn)
104104
expect_quick_identical(fn, 1:10)
105105
})
106+
107+
test_that("single-statement while/repeat bodies re-run their hoisted statements", {
108+
# A non-`{` loop body whose lone statement hoists code (here a BLAS
109+
# call) must emit that code inside the loop; hoisting it out of the
110+
# loop would freeze the body's work at its first evaluation. `for` is
111+
# covered in test-for-iterables.R.
112+
#
113+
# Run-tested only via the snapshots: a regression in either would
114+
# compute the product once before the loop and never terminate.
115+
squarings_while <- function(m) {
116+
declare(type(m = double(2, 2)))
117+
while (m[1, 1] < 100) m <- m %*% m
118+
m
119+
}
120+
121+
expect_translation_snapshots(squarings_while)
122+
123+
squarings_repeat <- function(m) {
124+
declare(type(m = double(2, 2)))
125+
repeat m <- m %*% m
126+
m
127+
}
128+
129+
expect_translation_snapshots(squarings_repeat)
130+
})

0 commit comments

Comments
 (0)