Skip to content

[7/15] BLAS/LAPACK lowerings run on unverified shapes: compile-time warning (or spurious error), then unchecked calls #128

Description

@mns-nordicals

What happens

When matrix dimensions can't be compared at compile time, the linear-algebra
paths diverge in three inconsistent ways — none of which prevents the bad
call:

1. %*% and triangular solve warn at compile time, then proceed
unchecked.

fn <- function(m, x) {
  declare(type(m = double(3, 3)), type(x = double(NA)))
  m %*% x
}
qfn <- quick(fn)
#> Warning: cannot verify conformability in %*% at compile time: 3L vs x__len_

fn(diag(3), as.double(1:2))       # R: Error: non-conformable arguments
qfn(diag(3), as.double(1:2))
#>      [,1]
#> [1,]  1.00000e+00
#> [2,]  2.00000e+00
#> [3,]  5.30499e-315     <- dgemv read past the end of x

The warning fires once, at compile time, for a condition that is only
sometimes a problem (the caller may always pass conformable inputs), and
the generated code then feeds unchecked dimensions straight into BLAS.
Depending on which dimension disagrees, that is either a silent
out-of-bounds read (above) or a cryptic abort from R's BLAS wrapper
("BLAS/LAPACK routine 'DGEMM ' gave error code -10") — never R's
"non-conformable arguments". solve()/qr.solve() right-hand sides and
the square-matrix checks in solve(a), chol(), chol2inv() behave the
same way.

2. crossprod()/tcrossprod() hard-error at compile time.

fn <- function(x, y) {
  declare(type(x = double(n, k)), type(y = double(m, j)))
  crossprod(x, y)
}
quick(fn)
#> Error: cannot verify conformability in crossprod at compile time

This rejects programs that are perfectly fine at run time (the caller
passes conformable matrices; the dims just aren't statically provable) —
the opposite failure mode of problem 1 above, in the same file.

3. Two matrices declared with NA dims are treated as equal. The
static comparison uses identical() on the dim expressions, and
identical(NA, NA) is TRUE — so double(NA, NA) against
double(NA, NA) skips even the warning and goes straight to the
unchecked BLAS call.

Why it happens

check_conformable() (R/r2f-matrix-blas.R) returns an ok/unknown
verdict, but each caller picks its own policy for unknown:
warn_conformability_unknown() + proceed at the %*%/gemv/gemm sites and
in assert_conformable_dims()/assert_square_matrix(); a hard stop()
in crossprod_like() (R/r2f-matrix.R). The runtime error machinery these
checks need (emit_quickr_error_if(), quickr_set_error_msg) already
exists and is used for LAPACK info codes in the same functions.

Expected behavior

One policy, matching the elementwise operators: statically known
mismatches stay compile errors; dimensions that can't be verified
statically get a runtime guard — one scalar size() comparison
emitted before the BLAS/LAPACK call, raising the same error R raises
("non-conformable arguments in %*%") instead of reading out of bounds.
The compile-time warning is dropped (it fires on a per-call-site
condition the compiler can't decide, and the guard now decides it
per call), and crossprod/tcrossprod relax from the spurious compile
error to the same guard — strictly more programs compile, all safely.
NA dims are always treated as unverified, never as equal.

cbind/rbind keep their compile error on an unknowable common
dimension: that dimension is needed to declare the output, which no
runtime guard can conjure.

The guard must be rank-aware. vector %*% vector with unknown lengths
reaches the gemm fallthrough, so a guard hardcoded to rank-2 axes would
emit size(x, 2) on a rank-1 array — invalid Fortran, which would make a
perfectly conformable unknown-length dot product fail to build. Rank-1
operands guard by their whole size instead.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions