Skip to content

Commit 2c06bb4

Browse files
committed
Refuse reassignment between scalar and array shapes
check_assignment_compatible() exempted any assignment where either side was length 1, so the two shape changes R handles by rebinding the symbol went undiagnosed in both directions: x <- numeric(n); x <- 0 broadcast 0 across every element (R: length 1) x <- 1; x <- numeric(3) kept only the first element (R: length 3) Only the both-sides-length-1 case is genuinely compatible (a declared double(1) is rank 1, a literal is rank 0), so exempt that and let everything else reach the shape checks. Deferred-shape locals still reallocate for an array value, as before. Three declare()-size-expression tests assigned a scalar into an array target as a way of reaching the size validator; they now use the same size expression on both sides, which reaches the intended error without depending on the exemption.
1 parent 3f38faa commit 2c06bb4

7 files changed

Lines changed: 93 additions & 16 deletions

File tree

NEWS.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919
`x <- numeric(2); x <- numeric(3)`) is a compile-time error;
2020
dimensions that cannot be compared at compile time are checked at run
2121
time. Previously such reassignments silently kept the old shape or
22-
produced invalid Fortran. Assigning a scalar into an array variable
23-
(native Fortran broadcast) is unchanged, as are locals declared with
24-
unknown (`NA`) dims, which reallocate on assignment like R.
22+
produced invalid Fortran. This also covers reassignment between a scalar
23+
and an array in either direction: `x <- numeric(n); x <- 0` used to
24+
broadcast the scalar across every element, and assigning an array to a
25+
scalar variable used to keep only its first element, where R rebinds the
26+
symbol in both cases. Locals declared with unknown (`NA`) dims still
27+
reallocate on assignment, like R.
2528

2629
- Elementwise operations (arithmetic, comparisons, `&`, `|`) now require
2730
operand lengths to match, unless one operand is a scalar or a vector is

R/r2f-operators-helpers.R

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,38 @@ check_assignment_compatible <- function(
626626
) {
627627
return(invisible())
628628
}
629-
if (passes_as_scalar(target) || passes_as_scalar(value)) {
629+
target_scalar <- passes_as_scalar(target)
630+
value_scalar <- passes_as_scalar(value)
631+
# Two length-1 values conform whatever their ranks (a declared `double(1)`
632+
# is rank 1; a literal is rank 0).
633+
if (target_scalar && value_scalar) {
630634
return(invisible())
631635
}
636+
deferred_local <- !target@is_external && has_self_size_dims(target)
637+
if (target_scalar || value_scalar) {
638+
# A deferred-shape local can genuinely take a new array shape.
639+
if (deferred_local && !value_scalar) {
640+
return(invisible())
641+
}
642+
# Otherwise one side is length 1 and the other is a real array. R
643+
# rebinds the symbol to the new shape; Fortran cannot, and would
644+
# silently broadcast a scalar across the array (or drop all but the
645+
# first element of an array into a scalar).
646+
stop(
647+
"cannot reassign `",
648+
name,
649+
"`: replacement is ",
650+
if (value_scalar) "a scalar" else "an array",
651+
" but `",
652+
name,
653+
"` is ",
654+
if (target_scalar) "a scalar" else "an array",
655+
"; R would rebind `",
656+
name,
657+
"` to the new shape",
658+
call. = FALSE
659+
)
660+
}
632661
if (target@rank != value@rank) {
633662
stop(
634663
"cannot reassign `",
@@ -643,7 +672,7 @@ check_assignment_compatible <- function(
643672
call. = FALSE
644673
)
645674
}
646-
if (!target@is_external && has_self_size_dims(target)) {
675+
if (deferred_local) {
647676
# deferred-shape local: implicit (re)allocation matches R's rebind
648677
return(invisible())
649678
}

tests/testthat/_snaps/size-constraint.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Output
66
function(a, b) {
77
declare(type(a = double(n)), type(b = double(n + 1)))
8-
a <- sum(b)
8+
a <- a + sum(b)
99
a
1010
}
1111
<environment: 0x0>
@@ -26,7 +26,7 @@
2626
! manifest end
2727
2828
29-
a = sum(b)
29+
a = (a + sum(b))
3030
end subroutine
3131
Code
3232
cat(cwrapper)

tests/testthat/test-assignment-shape.R

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,55 @@ test_that("shape-preserving reassignments still compile", {
8787
}
8888
expect_quick_identical(fn_same, c(1, 2, 3))
8989

90-
# scalar broadcast into an array target keeps working
91-
fn_scalar <- function(a) {
90+
# two length-1 values conform whatever their ranks: a declared double(1)
91+
# is rank 1, a literal is rank 0
92+
fn_len1 <- function(a) {
93+
declare(type(a = double(1)))
94+
a <- 2
95+
a
96+
}
97+
expect_quick_identical(fn_len1, 1)
98+
})
99+
100+
test_that("reassignment between scalar and array shapes is refused", {
101+
# R rebinds `x` to the scalar; Fortran would broadcast it across the
102+
# array, so every element would change instead of the shape
103+
fn_scalar_into_array <- function(a) {
92104
declare(type(a = double(n)))
93105
x <- a
94106
x <- 0
95107
sum(x)
96108
}
97-
expect_no_error(r2f(fn_scalar))
109+
expect_error(
110+
r2f(fn_scalar_into_array),
111+
"replacement is a scalar but `x` is an array",
112+
fixed = TRUE
113+
)
114+
115+
# the reduction form of the same mistake
116+
fn_reduce_into_array <- function(a) {
117+
declare(type(a = double(n)))
118+
x <- a
119+
x <- sum(x)
120+
x
121+
}
122+
expect_error(
123+
r2f(fn_reduce_into_array),
124+
"replacement is a scalar but `x` is an array",
125+
fixed = TRUE
126+
)
127+
128+
# the other direction: R rebinds to the array, Fortran would keep only
129+
# the first element
130+
fn_array_into_scalar <- function(a) {
131+
declare(type(a = double(3)))
132+
x <- 1
133+
x <- a
134+
x
135+
}
136+
expect_error(
137+
r2f(fn_array_into_scalar),
138+
"replacement is an array but `x` is a scalar",
139+
fixed = TRUE
140+
)
98141
})

tests/testthat/test-size-constraint.R

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ skip_on_cran()
55
test_that("size constraint", {
66
fn <- function(a, b) {
77
declare(type(a = double(n)), type(b = double(n + 1)))
8-
a <- sum(b)
8+
a <- a + sum(b)
99
a
1010
}
1111

@@ -20,7 +20,9 @@ test_that("size constraint", {
2020
fixed = TRUE
2121
)
2222
expect_translation_snapshots(fn, "call_size_constraint")
23-
expect_equal(qfn(1, c(2, 3)), 5)
23+
# `a` stays an array: reassigning a scalar into it would be a shape
24+
# change, which R does by rebinding and Fortran cannot do at all
25+
expect_equal(qfn(1, c(2, 3)), 6)
2426
})
2527

2628
test_that("size constraint", {

tests/testthat/test-size-expr-abs.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test_that("declare() size expressions validate abs() arity", {
2929
type(m = integer(1)),
3030
type(out = double(abs(n, m)))
3131
)
32-
out <- double(1L)
32+
out <- double(abs(n, m))
3333
out
3434
}
3535

tests/testthat/test-size-expr-dim-nrow-ncol.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ test_that("declare() size expressions support dim(x)[axis] indices", {
4242
test_that("declare() size expressions validate nrow()/ncol() arity", {
4343
bad_nrow <- function(x) {
4444
declare(type(x = double(NA, NA)), type(out = double(nrow(x, 1L))))
45-
out <- double(1L)
45+
out <- double(nrow(x, 1L))
4646
out
4747
}
4848
bad_ncol <- function(x) {
4949
declare(type(x = double(NA, NA)), type(out = double(ncol(x, 1L))))
50-
out <- double(1L)
50+
out <- double(ncol(x, 1L))
5151
out
5252
}
5353

@@ -68,7 +68,7 @@ test_that("declare() size expressions reject ncol() beyond variable rank", {
6868
test_that("declare() size expressions reject unsupported calls", {
6969
bad <- function(n) {
7070
declare(type(n = integer(1)), type(out = double(sum(n))))
71-
out <- double(1L)
71+
out <- double(sum(n))
7272
out
7373
}
7474

0 commit comments

Comments
 (0)