Skip to content

Commit 4620eaf

Browse files
committed
Require a square matrix in solve(), matching R
solve(a, b) with a rectangular a fell through to a least-squares dgels call, returning qr.solve()'s answer where R raises "'a' (m x n) must be square". Statically rectangular systems are now a compile error and symbolic squareness is guarded at run time before the dgesv call, via the assert_square_matrix() helper the other LAPACK lowerings already use. The now-unreachable rectangular tail of lapack_solve() (dgels, and a dgelsy branch qr.solve() never reached) is deleted; qr.solve() keeps its least-squares behavior. The solve output follows ncol(a) while b follows nrow(a); when ncol is statically 1 the output declares as a Fortran scalar, so a symbolic-length b is copied elementwise instead of by whole-array assignment.
1 parent 59469f0 commit 4620eaf

3 files changed

Lines changed: 55 additions & 175 deletions

File tree

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
build even in arithmetic, and a symbolic-length `x + m` returned a plain
3333
vector where R returns a `1x1` matrix.
3434

35+
- `solve(a, b)` now requires a square `a`, matching R. A rectangular `a`
36+
previously fell through to a least-squares solve (dgels), returning an
37+
answer where R raises `'a' (m x n) must be square`. Statically
38+
rectangular systems are now a compile-time error; when squareness is not
39+
known at compile time it is checked at run time. Use `qr.solve()` for
40+
least-squares solutions of rectangular systems (unchanged).
41+
3542
# quickr 0.3.0
3643

3744
This release adds major new support for linear algebra, local functions,

R/r2f-matrix-blas.R

Lines changed: 17 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -643,13 +643,12 @@ lapack_solve <- function(
643643

644644
nrhs <- if (b_rank == 1L) 1L else dim_or_one(B, 2L)
645645

646-
# solve(a, b) with a rectangular `a` deliberately falls through to the
647-
# least-squares branch below -- a divergence from base R (which requires
648-
# a square `a`), locked by the "least-squares" tests in
649-
# test-matrix-lapack.R. Squareness is a routing decision here, not a
650-
# correctness guard: unknown squareness routes to dgels, which solves
651-
# square systems exactly too.
652-
if (dims_match(m, n) && !identical(context, "qr.solve")) {
646+
# R's solve() requires a square `a`; least squares is qr.solve()'s job.
647+
# Statically rectangular `a` is a compile error, symbolic dims get a
648+
# runtime guard before the dgesv call. (A rectangular `a` used to fall
649+
# through to a dgels least-squares solve -- an answer where R errors.)
650+
if (!identical(context, "qr.solve")) {
651+
assert_square_matrix(a_dims, A, context, hoist, scope)
653652
A_work <- hoist$declare_tmp(mode = "double", dims = list(m, m))
654653
hoist$emit(glue("{A_work@name} = {A_name}"))
655654

@@ -671,7 +670,17 @@ lapack_solve <- function(
671670
out_var <- hoist$declare_tmp(mode = "double", dims = expected_dims)
672671
out_name <- out_var@name
673672
}
674-
hoist$emit(glue("{out_name} = {B_input_name}"))
673+
# The output length follows ncol(a) (R's contract) while `b` follows
674+
# nrow(a); the two are only runtime-equal. When ncol is statically 1
675+
# the output declares as a scalar, so a symbolic-length `b` must be
676+
# copied elementwise, not by whole-array assignment.
677+
b_src <- if (passes_as_scalar(out_var) && !passes_as_scalar(B@value)) {
678+
subs <- str_flatten_commas(rep("1", b_rank))
679+
glue("{B_input_name}({subs})")
680+
} else {
681+
B_input_name
682+
}
683+
hoist$emit(glue("{out_name} = {b_src}"))
675684

676685
ipiv <- hoist$declare_tmp(mode = "integer", dims = list(m))
677686
info <- hoist$declare_tmp(mode = "integer", dims = NULL)
@@ -813,121 +822,6 @@ end do"
813822
}
814823
return(out)
815824
}
816-
817-
A_work <- hoist$declare_tmp(mode = "double", dims = list(m, n))
818-
hoist$emit(glue("{A_work@name} = {A_name}"))
819-
820-
max_mn <- call("max", m, n)
821-
822-
B_work <- hoist$declare_tmp(mode = "double", dims = list(max_mn, nrhs))
823-
m_f <- dims2f(list(m), scope)
824-
if (!nzchar(m_f)) {
825-
m_f <- "1"
826-
}
827-
n_f <- dims2f(list(n), scope)
828-
if (!nzchar(n_f)) {
829-
n_f <- "1"
830-
}
831-
nrhs_f <- dims2f(list(nrhs), scope)
832-
if (!nzchar(nrhs_f)) {
833-
nrhs_f <- "1"
834-
}
835-
hoist$emit(glue("{B_work@name} = 0.0_c_double"))
836-
if (b_rank == 1L) {
837-
hoist$emit(glue("{B_work@name}(1:{m_f}, 1) = {B_input_name}"))
838-
} else {
839-
hoist$emit(glue("{B_work@name}(1:{m_f}, 1:{nrhs_f}) = {B_input_name}"))
840-
}
841-
842-
info <- hoist$declare_tmp(mode = "integer", dims = NULL)
843-
844-
mn <- call("min", m, n)
845-
if (identical(context, "qr.solve")) {
846-
jpvt <- hoist$declare_tmp(mode = "integer", dims = list(n))
847-
hoist$emit(glue("{jpvt@name} = 0_c_int"))
848-
849-
rcond <- if (is.null(tol)) "1e-7_c_double" else as.character(tol)
850-
rank <- hoist$declare_tmp(mode = "integer", dims = NULL)
851-
852-
lwork <- call(
853-
"max",
854-
1L,
855-
call("+", mn, call("max", mn, nrhs)),
856-
call("+", call("*", 2L, mn), call("*", 64L, call("+", n, 1L))),
857-
call("+", mn, call("*", 2L, n))
858-
)
859-
work <- hoist$declare_tmp(mode = "double", dims = list(lwork))
860-
861-
hoist$emit(glue(
862-
"call dgelsy({blas_int(m)}, {blas_int(n)}, {blas_int(nrhs)}, {A_work@name}, {blas_int(m)}, {B_work@name}, {blas_int(max_mn)}, {jpvt@name}, {rcond}, {rank@name}, {work@name}, {blas_int(lwork)}, {info@name})"
863-
))
864-
emit_quickr_error_if(
865-
condition = glue("{info@name} < 0_c_int"),
866-
message = "Lapack routine dgelsy: illegal argument",
867-
hoist = hoist,
868-
scope = scope
869-
)
870-
emit_quickr_error_if(
871-
condition = glue("{info@name} > 0_c_int"),
872-
message = "Lapack routine dgelsy failed to converge",
873-
hoist = hoist,
874-
scope = scope
875-
)
876-
emit_quickr_error_if(
877-
condition = glue("{rank@name} < {blas_int(n)}"),
878-
message = "rank deficient matrix in qr.solve",
879-
hoist = hoist,
880-
scope = scope
881-
)
882-
} else {
883-
lwork <- call("max", 1L, call("+", mn, call("max", mn, nrhs)))
884-
work <- hoist$declare_tmp(mode = "double", dims = list(lwork))
885-
886-
hoist$emit(glue(
887-
"call dgels('N', {blas_int(m)}, {blas_int(n)}, {blas_int(nrhs)}, {A_work@name}, {blas_int(m)}, {B_work@name}, {blas_int(max_mn)}, {work@name}, {blas_int(lwork)}, {info@name})"
888-
))
889-
emit_quickr_error_if(
890-
condition = glue("{info@name} < 0_c_int"),
891-
message = "Lapack routine dgels: illegal argument",
892-
hoist = hoist,
893-
scope = scope
894-
)
895-
}
896-
897-
expected_dims <- if (b_rank == 1L) list(n) else list(n, nrhs)
898-
writes_to_dest <- FALSE
899-
if (
900-
can_use_output(
901-
dest,
902-
input_names = c(A_name, B_input_name),
903-
expected_dims = expected_dims,
904-
context = context,
905-
allow_alias = B_input_name
906-
)
907-
) {
908-
out_var <- dest
909-
out_name <- dest@name
910-
writes_to_dest <- TRUE
911-
} else {
912-
out_var <- hoist$declare_tmp(mode = "double", dims = expected_dims)
913-
out_name <- out_var@name
914-
}
915-
916-
if (b_rank == 1L) {
917-
if (passes_as_scalar(out_var)) {
918-
hoist$emit(glue("{out_name} = {B_work@name}(1, 1)"))
919-
} else {
920-
hoist$emit(glue("{out_name} = {B_work@name}(1:{n_f}, 1)"))
921-
}
922-
} else {
923-
hoist$emit(glue("{out_name} = {B_work@name}(1:{n_f}, 1:{nrhs_f})"))
924-
}
925-
926-
out <- Fortran(out_name, out_var)
927-
if (writes_to_dest) {
928-
out@writes_to_dest <- TRUE
929-
}
930-
out
931825
}
932826

933827
lapack_inverse <- function(A, scope, hoist, dest = NULL, context = "solve") {

tests/testthat/test-matrix-lapack.R

Lines changed: 31 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -62,66 +62,55 @@ test_that("solve handles column RHS matrices and 1x1 systems", {
6262
expect_quick_equal(solve_scalar, list(A = matrix(2.5, 1L, 1L), b = 1.25))
6363
})
6464

65-
test_that("solve supports least-squares for rectangular systems", {
66-
solve_ls_vec <- function(X, y) {
65+
test_that("solve requires a square coefficient matrix, like R", {
66+
# Squareness unknown at compile time: a runtime guard runs before dgesv.
67+
solve_sym <- function(X, y) {
6768
declare(
6869
type(X = double(n, k)),
6970
type(y = double(n))
7071
)
7172
solve(X, y)
7273
}
7374

74-
solve_ls_mat <- function(X, Y) {
75-
declare(
76-
type(X = double(n, k)),
77-
type(Y = double(n, p))
78-
)
79-
solve(X, Y)
80-
}
81-
8275
set.seed(123)
8376
n <- 20
8477
k <- 5
85-
p <- 3
8678
X <- matrix(rnorm(n * k), n, k)
8779
y <- rnorm(n)
88-
Y <- matrix(rnorm(n * p), n, p)
8980

90-
q_solve_ls_vec <- expect_warning(quick(solve_ls_vec), NA)
91-
q_solve_ls_mat <- expect_warning(quick(solve_ls_mat), NA)
81+
q_solve_sym <- quick(solve_sym)
82+
expect_error(q_solve_sym(X, y), "solve requires a square matrix")
83+
expect_error(solve(X, y), "must be square") # the R oracle errors too
9284

93-
expect_equal(q_solve_ls_vec(X, y), qr.solve(X, y))
94-
expect_equal(q_solve_ls_mat(X, Y), qr.solve(X, Y))
95-
})
85+
base <- matrix(rnorm(n * n), n, n)
86+
A <- crossprod(base) + diag(n)
87+
expect_equal(q_solve_sym(A, y), solve(A, y))
9688

97-
test_that("solve supports least-squares for single-column systems", {
98-
solve_ls_col <- function(X, y) {
89+
# One statically known axis still guards the symbolic one.
90+
solve_col <- function(X, y) {
9991
declare(
10092
type(X = double(n, 1L)),
10193
type(y = double(n))
10294
)
10395
solve(X, y)
10496
}
105-
106-
set.seed(125)
107-
n <- 20
108-
X <- matrix(rnorm(n), n, 1L)
109-
y <- rnorm(n)
110-
111-
q_solve_ls_col <- expect_warning(quick(solve_ls_col), NA)
112-
expect_equal(q_solve_ls_col(X, y), qr.solve(X, y))
97+
q_solve_col <- quick(solve_col)
98+
expect_error(q_solve_col(X[, 1L, drop = FALSE], y), "square matrix")
99+
expect_equal(
100+
q_solve_col(matrix(2.5, 1L, 1L), 1.25),
101+
solve(matrix(2.5, 1L, 1L), 1.25)
102+
)
113103
})
114104

115-
test_that("solve compiles 1-row least-squares systems", {
105+
test_that("solve rejects statically rectangular systems at compile time", {
116106
solve_one_row <- function(X, y) {
117107
declare(
118108
type(X = double(1L, 2L)),
119109
type(y = double(1L))
120110
)
121111
solve(X, y)
122112
}
123-
124-
expect_no_error(r2f(solve_one_row))
113+
expect_error(r2f(solve_one_row), "solve requires a square matrix")
125114
})
126115

127116
test_that("qr.solve matches R for vectors and matrices", {
@@ -195,7 +184,7 @@ test_that("qr.solve uses QR with pivoting for known square systems", {
195184
expect_false(has_call(square_fortran, "dgesv"))
196185
})
197186

198-
test_that("solve uses dgesv for known square and dgels for rectangular systems", {
187+
test_that("solve always uses dgesv, guarding squareness when symbolic", {
199188
solve_square <- function(A, b) {
200189
declare(
201190
type(A = double(n, n)),
@@ -212,12 +201,7 @@ test_that("solve uses dgesv for known square and dgels for rectangular systems",
212201
solve(A, b)
213202
}
214203

215-
solve_rect <- function(A, b) {
216-
declare(type(A = double(3, 2)), type(b = double(3)))
217-
solve(A, b)
218-
}
219-
220-
solve_rect_named <- function(A, b) {
204+
solve_sym <- function(A, b) {
221205
declare(
222206
type(A = double(n, k)),
223207
type(b = double(n))
@@ -233,32 +217,27 @@ test_that("solve uses dgesv for known square and dgels for rectangular systems",
233217
capture.output(cat(r2f(solve_square_fixed))),
234218
collapse = "\n"
235219
)
236-
rect_fixed_fortran <- paste(
237-
capture.output(cat(r2f(solve_rect))),
238-
collapse = "\n"
239-
)
240-
rect_named_fortran <- paste(
241-
capture.output(cat(r2f(solve_rect_named))),
220+
sym_fortran <- paste(
221+
capture.output(cat(r2f(solve_sym))),
242222
collapse = "\n"
243223
)
244224

245225
has_call <- function(code, routine) {
246226
any(grepl(paste0("call ", routine, "("), tolower(code), fixed = TRUE))
247227
}
248228

249-
# Search emitted Fortran to ensure solve() chooses LU (dgesv) for proven-square
250-
# systems and least-squares (dgels) for rectangular systems.
229+
# solve() is LU (dgesv) only; least squares is qr.solve()'s job. Provably
230+
# square systems get no guard, symbolic squareness is checked at run time.
251231
expect_true(has_call(square_named_fortran, "dgesv"))
252232
expect_false(has_call(square_named_fortran, "dgels"))
233+
expect_false(grepl("square matrix", square_named_fortran, fixed = TRUE))
253234

254235
expect_true(has_call(square_fixed_fortran, "dgesv"))
255236
expect_false(has_call(square_fixed_fortran, "dgels"))
256237

257-
expect_true(has_call(rect_fixed_fortran, "dgels"))
258-
expect_false(has_call(rect_fixed_fortran, "dgesv"))
259-
260-
expect_true(has_call(rect_named_fortran, "dgels"))
261-
expect_false(has_call(rect_named_fortran, "dgesv"))
238+
expect_true(has_call(sym_fortran, "dgesv"))
239+
expect_false(has_call(sym_fortran, "dgels"))
240+
expect_match(sym_fortran, "solve requires a square matrix", fixed = TRUE)
262241
})
263242

264243
test_that("chol and chol2inv match R", {
@@ -415,7 +394,7 @@ test_that("Test bad path in lapack functions", {
415394
}
416395

417396
expect_error(quick(solve_bad_rank), "expects a matrix")
418-
expect_warning(quick(solve_non_square), NA)
397+
expect_error(quick(solve_non_square), "requires a square matrix")
419398
expect_error(quick(solve_bad_rhs), "only supports vector or matrix")
420399
expect_error(quick(chol_bad_rank), "expects a matrix")
421400
expect_error(quick(chol_pivot), "pivot = TRUE")
@@ -446,7 +425,7 @@ test_that("lapack functions test non-square matrix errors", {
446425
chol2inv(R)
447426
}
448427

449-
expect_warning(quick(solve_rect), NA)
428+
expect_error(quick(solve_rect), "requires a square matrix")
450429
expect_error(quick(chol_rect), "requires a square matrix")
451430
expect_error(quick(chol2inv_rect), "requires a square matrix")
452431
})

0 commit comments

Comments
 (0)