Skip to content

Commit c6004a1

Browse files
committed
Refuse complex operands in linear algebra
The BLAS/LAPACK paths cast logical/integer operands to double via maybe_cast_double(), which passes complex through untouched -- so complex operands flowed into the real d* routines, which read complex storage as reals: complex(2) %*% complex(2) returned a real dot product of the real parts where R returns the complex result. A silent wrong answer, found in external review. All linalg operand casts now go through cast_linalg_double(), which refuses complex with a compile-time message naming the divergence (linear algebra in quickr is double-only). Covers %*% (including t() forms), crossprod/tcrossprod, solve/qr.solve, forwardsolve/backsolve, chol, chol2inv, svd, and outer. The mode-preserving standalone t() and elementwise complex arithmetic are untouched.
1 parent 885ec26 commit c6004a1

5 files changed

Lines changed: 81 additions & 15 deletions

File tree

R/r2f-matrix-blas.R

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,8 @@ outer_mul <- function(
471471
) {
472472
assert_hoist_env(hoist)
473473

474-
x <- maybe_cast_double(x)
475-
y <- maybe_cast_double(y)
474+
x <- cast_linalg_double(x, context)
475+
y <- cast_linalg_double(y, context)
476476

477477
if (x@value@rank > 1L || y@value@rank > 1L) {
478478
stop("outer() only supports vectors or scalars")
@@ -523,8 +523,8 @@ triangular_solve <- function(
523523
) {
524524
assert_hoist_env(hoist)
525525

526-
A <- maybe_cast_double(A)
527-
B <- maybe_cast_double(B)
526+
A <- cast_linalg_double(A, context)
527+
B <- cast_linalg_double(B, context)
528528

529529
assert_rank2_matrix(A, "triangular solve expects a matrix")
530530

@@ -605,8 +605,8 @@ lapack_solve <- function(
605605
) {
606606
assert_hoist_env(hoist)
607607

608-
A <- maybe_cast_double(A)
609-
B <- maybe_cast_double(B)
608+
A <- cast_linalg_double(A, context)
609+
B <- cast_linalg_double(B, context)
610610

611611
assert_rank2_matrix(A, paste0(context, " expects a matrix for `a`"))
612612

@@ -812,7 +812,7 @@ end do"
812812
lapack_inverse <- function(A, scope, hoist, dest = NULL, context = "solve") {
813813
assert_hoist_env(hoist)
814814

815-
A <- maybe_cast_double(A)
815+
A <- cast_linalg_double(A, context)
816816
assert_rank2_matrix(A, paste0(context, " expects a matrix for `a`"))
817817

818818
a_dims <- matrix_dims(A)
@@ -886,7 +886,7 @@ lapack_inverse <- function(A, scope, hoist, dest = NULL, context = "solve") {
886886
lapack_chol <- function(A, scope, hoist, dest = NULL, context = "chol") {
887887
assert_hoist_env(hoist)
888888

889-
A <- maybe_cast_double(A)
889+
A <- cast_linalg_double(A, context)
890890
assert_rank2_matrix(A, paste0(context, " expects a matrix"))
891891

892892
a_dims <- matrix_dims(A)
@@ -949,7 +949,7 @@ lapack_chol2inv <- function(
949949
) {
950950
assert_hoist_env(hoist)
951951

952-
R <- maybe_cast_double(R)
952+
R <- cast_linalg_double(R, context)
953953
assert_rank2_matrix(R, paste0(context, " expects a matrix"))
954954

955955
r_dims <- matrix_dims(R)
@@ -1161,7 +1161,7 @@ lapack_svd <- function(
11611161
assert_hoist_env(hoist)
11621162
stopifnot(inherits(d, Variable), inherits(u, Variable), inherits(v, Variable))
11631163

1164-
A <- maybe_cast_double(A)
1164+
A <- cast_linalg_double(A, context)
11651165
dims <- svd_dims(A, context = context)
11661166
m <- dims$m
11671167
n <- dims$n

R/r2f-matrix-parse.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ unwrap_transpose_arg <- function(arg, scope, ..., hoist) {
99
if (is_call(arg_unwrapped, quote(t)) && length(arg_unwrapped) == 2L) {
1010
inner_arg <- unwrap_parens(arg_unwrapped[[2L]])
1111
inner <- r2f(inner_arg, scope, ..., hoist = hoist)
12-
inner <- maybe_cast_double(inner)
12+
inner <- cast_linalg_double(inner, "%*%")
1313
if (inner@value@rank == 2L) {
1414
return(list(value = inner, trans = "T"))
1515
} else if (inner@value@rank == 1L) {
@@ -26,7 +26,7 @@ unwrap_transpose_arg <- function(arg, scope, ..., hoist) {
2626
}
2727
}
2828
value <- r2f(arg, scope, ..., hoist = hoist)
29-
value <- maybe_cast_double(value)
29+
value <- cast_linalg_double(value, "%*%")
3030
list(value = value, trans = "N")
3131
}
3232

R/r2f-matrix.R

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,10 @@ register_r2f_handler(
627627
tol <- if (is.null(tol_arg) || is_missing(tol_arg)) {
628628
r2f(1e-7, scope, ..., hoist = hoist)
629629
} else {
630-
tol <- maybe_cast_double(r2f(tol_arg, scope, ..., hoist = hoist))
630+
tol <- cast_linalg_double(
631+
r2f(tol_arg, scope, ..., hoist = hoist),
632+
"qr.solve"
633+
)
631634
if (tol@value@rank != 0L) {
632635
stop("qr.solve() expects a scalar `tol`", call. = FALSE)
633636
}
@@ -916,7 +919,7 @@ crossprod_like <- function(
916919
context
917920
) {
918921
x <- r2f(x_arg, scope, ..., hoist = hoist)
919-
x <- maybe_cast_double(x)
922+
x <- cast_linalg_double(x, context)
920923

921924
if (is.null(y_arg)) {
922925
return(syrk(
@@ -929,7 +932,7 @@ crossprod_like <- function(
929932
))
930933
}
931934

932-
y <- maybe_cast_double(r2f(y_arg, scope, ..., hoist = hoist))
935+
y <- cast_linalg_double(r2f(y_arg, scope, ..., hoist = hoist), context)
933936

934937
x_dims <- matrix_dims(x)
935938
y_dims <- matrix_dims(y)

R/r2f-operators-helpers.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ maybe_cast_double <- function(x) {
8585
}
8686
}
8787

88+
# Cast a linear-algebra operand to double for the real BLAS/LAPACK
89+
# lowerings (dgemm, dgesv, ...). Complex operands are refused: the d*
90+
# routines would read complex storage as reals and return a plausible
91+
# wrong answer, and quickr has no z* lowerings. R supports complex
92+
# linear algebra, so the message names the divergence.
93+
# Used by: r2f-matrix.R, r2f-matrix-parse.R, r2f-matrix-blas.R
94+
cast_linalg_double <- function(x, context) {
95+
if (identical(x@value@mode, "complex")) {
96+
stop(
97+
context,
98+
" does not support complex operands; ",
99+
"linear algebra in quickr is double-only",
100+
call. = FALSE
101+
)
102+
}
103+
maybe_cast_double(x)
104+
}
105+
88106
# Promote a list of operands to their common (lattice-join) mode, casting
89107
# each one whose mode differs. For contexts where Fortran requires uniform
90108
# argument types: array constructors (c()), min/max, merge, modulo.

tests/testthat/test-errors.R

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,48 @@ test_that("unsupported complex operations are refused with R's messages", {
223223
}
224224
expect_error(quick(complex_mod), "unimplemented complex operation")
225225
})
226+
227+
test_that("complex operands are refused in linear algebra", {
228+
# The real BLAS/LAPACK lowerings (dgemm, dgesv, ...) would read complex
229+
# storage as reals and return a plausible wrong answer where R returns a
230+
# complex result: complex(2) %*% complex(2) returned a real dot product
231+
# of the real parts. Refuse at compile time instead.
232+
complex_matmul <- function(x, y) {
233+
declare(type(x = complex(2)), type(y = complex(2)))
234+
x %*% y
235+
}
236+
expect_error(
237+
quick(complex_matmul),
238+
"%*% does not support complex operands",
239+
fixed = TRUE
240+
)
241+
242+
# One complex operand is enough to poison the d* routine.
243+
complex_mixed <- function(x, y) {
244+
declare(type(x = complex(2, 2)), type(y = double(2, 2)))
245+
x %*% y
246+
}
247+
expect_error(quick(complex_mixed), "does not support complex operands")
248+
249+
complex_solve <- function(x) {
250+
declare(type(x = complex(2, 2)))
251+
solve(x)
252+
}
253+
expect_error(quick(complex_solve), "does not support complex operands")
254+
255+
complex_crossprod <- function(x) {
256+
declare(type(x = complex(2, 2)))
257+
crossprod(x)
258+
}
259+
expect_error(quick(complex_crossprod), "does not support complex operands")
260+
261+
# t() alone is mode-preserving and keeps working on complex values.
262+
complex_t <- function(x) {
263+
declare(type(x = complex(2, 2)))
264+
t(x)
265+
}
266+
expect_quick_identical(
267+
complex_t,
268+
list(matrix(c(1 + 1i, 2 + 0i, 3 - 1i, 4 + 2i), 2, 2))
269+
)
270+
})

0 commit comments

Comments
 (0)