What happens
Four holes in how elementwise operations and constructors handle shapes.
1. Vector operands of different lengths silently truncate. R recycles
(with a warning when lengths aren't multiples); Fortran assigns through the
shape of the destination:
fn <- function(a, b) {
declare(type(a = double(n)), type(b = double(m)))
a + b
}
fn(c(1, 2), c(10, 20, 30, 40)) #> c(11, 22, 31, 42) (recycles, warns)
quick(fn)(c(1, 2), c(10, 20, 30, 40)) #> c(11, 22) -- silent truncation
The generated statement is out_ = (a + b) with out_ declared length
n — elements past min(n, m) of the longer operand are simply never
read, and nothing is diagnosed. When both lengths are known constants the
mismatch surfaces, but as a raw gfortran "Shapes for operands are not
conformable" build failure rather than a quickr diagnostic — and
check_recyclable_pair() explicitly blesses known divisible lengths
(length 2 vs length 4) that it has no code to implement. Comparison
operators (<, ==, ...), &/|, %%, and %/% don't consult any
length check at all, so the same silent truncation applies there.
2. Fill constructors leak a single scalar into c().
fn <- function(x) {
declare(type(x = double(3)))
c(numeric(2), x)
}
fn(as.double(1:3)) #> c(0, 0, 1, 2, 3)
numeric(2) lowers to the scalar text 0 with a claimed length of 2, so
c() emits out_ = [ 0, x ] — four elements where the declared output
length is five. Today this happens to fail the build (the untyped integer
literal 0 next to a double x makes a mixed-type constructor gfortran
rejects), but that is an accident of the second bug hiding the first: the
fill literals are also mode-wrong (0 for numeric(k) instead of
0.0_c_double), which additionally poisons any other context that splices
them.
3. matrix(scalar, m, n) is a scalar wearing rank-2 dims.
fn <- function() {
sum(matrix(2, 2, 3))
}
fn() #> 12
quick(fn) generates out_ = sum(2.0_c_double) — the handler rebadges the
scalar's Variable with dims (2, 3) and returns the scalar expression
text. That works only where Fortran's scalar broadcast applies (direct
whole-array assignment); inside any intrinsic that requires an array it is
invalid Fortran and the build fails.
4. 1×1 matrices against vectors: builds fail where R answers, and the one
case that does work is shape-wrong. quickr scalarizes a 1×1-matrix operand
so it broadcasts like a scalar — but only in arithmetic, the one operator
class that consults a shape check at all. Three distinct problems:
-
When the 1×1 operand needs a cast or booleanization, the scalarizer
appends (1, 1) to the already wrapped expression text:
! double(3) + logical(1, 1) -- legal in R, fails the build
out_ = (a + merge(1.0_c_double, 0.0_c_double, (b/=0))(1, 1))
gfortran rejects that as an unclassifiable statement.
-
Comparisons and &/| never reach the shape check, so a 1×1 operand
arrives at gfortran as a bare rank mismatch — out_ = (a < b) →
"Inconsistent ranks for operator". R refuses these too ("dims [product 1]
do not match the length of object"), so the verdict is right and only the
diagnostic is wrong. But the split matters going forward: R recycles a
length-1 array in arithmetic (deprecated, still working) and errors in
comparisons and &/|, so routing every operator class through one
shared shape check — which fixing defect 1 requires — would make
x < m11 start answering where R refuses.
-
Scalarizing is shape-wrong whenever the vector's length is only known at
run time. R keeps the 1×1 dims when the vector has length 1 and drops them
otherwise; the scalarized code always returns a plain vector, so
double(1, 1) + double(n) called with a length-1 vector returns 5 where
R returns matrix(5, 1, 1).
Why it happens
R/r2f-operators-helpers.R: check_recyclable_pair() returns ok = TRUE
for known divisible lengths (recycling that is never implemented), for any
zero length, and for differing symbolic lengths (unknown = TRUE, which
the caller maybe_reshape_vector_matrix() ignores). Arithmetic handlers
are the only ones that even call it.
R/r2f-constructors.R: the logical()/integer()/double()/numeric()
handlers return one scalar literal (untyped, and integer-typed for the
double fills) carrying array dims; c() splices the text but sizes the
result from the dims. array() already solves this with an implied-do
spread for fill constructors — c() never got the same treatment.
R/r2f-constructors.R: the matrix() handler's passes_as_scalar()
short-circuit returns the scalar text unchanged with rank-2 dims.
R/r2f-operators-helpers.R: scalarize_matrix() indexes whatever
expression text it is handed, so a cast wrapper becomes unindexable; and
maybe_reshape_vector_matrix() — which only the arithmetic handlers call —
applies it for any vector length not provably 1, including symbolic
lengths whose runtime value decides the result shape in R.
Expected behavior
- Operand lengths that are known to differ (including length 0, including
the divisible case) are a compile-time error: "elementwise vector
operations require equal lengths or a scalar operand; R-style recycling is
not supported". Scalar broadcast is unaffected. This applies uniformly to
arithmetic, comparisons, &/|, %%, and %/%. Implementing true
R recycling would need per-element modulo indexing and, for non-multiple
lengths, a value-dependent warning — poor value for the surface it adds;
an informative refusal keeps quick(f) honest.
- Lengths that cannot be compared at compile time (symbolic vs symbolic,
symbolic vs constant) get a runtime guard — one scalar size()
comparison per statement through the existing error machinery, raising
the same message as an ordinary R error. Identical symbolic length
expressions stay guard-free. The same policy covers elementwise
matrix-matrix operands axis by axis, and the vector-matrix row rule
(a vector combined column-wise with a matrix whose rows it spans),
which previously refused at compile time whenever the row count could
not be verified statically.
c(numeric(k), x) works and matches R: fills spread as implied-dos
([(0.0_c_double, i = 1, k), x]) with mode-correct literals.
matrix(scalar, m, n) works in any context: it materializes into a
hoisted rank-2 temporary where an array is required, and keeps today's
free scalar broadcast when directly assigned.
- 1×1-matrix operands follow R's split: arithmetic keeps the scalarized
broadcast when the vector's length is statically known and not 1
(hoisting cast wrappers to a temporary before indexing, so the cells R
supports compile), while comparisons and &/| treat the 1×1 as an
ordinary one-row matrix — known longer vectors are a compile error,
unknown lengths get the runtime guard (length 1 still conforms, as in
R). A vector whose length is only known at run time takes the
one-row-matrix rule in arithmetic too, since the result's shape would
otherwise depend on the runtime value: length 1 passes the guard and
yields a 1×1 matrix as in R, longer vectors error where R would
recycle (deprecated).
What happens
Four holes in how elementwise operations and constructors handle shapes.
1. Vector operands of different lengths silently truncate. R recycles
(with a warning when lengths aren't multiples); Fortran assigns through the
shape of the destination:
The generated statement is
out_ = (a + b)without_declared lengthn— elements pastmin(n, m)of the longer operand are simply neverread, and nothing is diagnosed. When both lengths are known constants the
mismatch surfaces, but as a raw gfortran "Shapes for operands are not
conformable" build failure rather than a quickr diagnostic — and
check_recyclable_pair()explicitly blesses known divisible lengths(
length 2vslength 4) that it has no code to implement. Comparisonoperators (
<,==, ...),&/|,%%, and%/%don't consult anylength check at all, so the same silent truncation applies there.
2. Fill constructors leak a single scalar into
c().numeric(2)lowers to the scalar text0with a claimed length of 2, soc()emitsout_ = [ 0, x ]— four elements where the declared outputlength is five. Today this happens to fail the build (the untyped integer
literal
0next to a doublexmakes a mixed-type constructor gfortranrejects), but that is an accident of the second bug hiding the first: the
fill literals are also mode-wrong (
0fornumeric(k)instead of0.0_c_double), which additionally poisons any other context that splicesthem.
3.
matrix(scalar, m, n)is a scalar wearing rank-2 dims.quick(fn)generatesout_ = sum(2.0_c_double)— the handler rebadges thescalar's Variable with dims
(2, 3)and returns the scalar expressiontext. That works only where Fortran's scalar broadcast applies (direct
whole-array assignment); inside any intrinsic that requires an array it is
invalid Fortran and the build fails.
4. 1×1 matrices against vectors: builds fail where R answers, and the one
case that does work is shape-wrong. quickr scalarizes a 1×1-matrix operand
so it broadcasts like a scalar — but only in arithmetic, the one operator
class that consults a shape check at all. Three distinct problems:
When the 1×1 operand needs a cast or booleanization, the scalarizer
appends
(1, 1)to the already wrapped expression text:gfortran rejects that as an unclassifiable statement.
Comparisons and
&/|never reach the shape check, so a 1×1 operandarrives at gfortran as a bare rank mismatch —
out_ = (a < b)→"Inconsistent ranks for operator". R refuses these too ("dims [product 1]
do not match the length of object"), so the verdict is right and only the
diagnostic is wrong. But the split matters going forward: R recycles a
length-1 array in arithmetic (deprecated, still working) and errors in
comparisons and
&/|, so routing every operator class through oneshared shape check — which fixing defect 1 requires — would make
x < m11start answering where R refuses.Scalarizing is shape-wrong whenever the vector's length is only known at
run time. R keeps the 1×1 dims when the vector has length 1 and drops them
otherwise; the scalarized code always returns a plain vector, so
double(1, 1) + double(n)called with a length-1 vector returns5whereR returns
matrix(5, 1, 1).Why it happens
R/r2f-operators-helpers.R:check_recyclable_pair()returnsok = TRUEfor known divisible lengths (recycling that is never implemented), for any
zero length, and for differing symbolic lengths (
unknown = TRUE, whichthe caller
maybe_reshape_vector_matrix()ignores). Arithmetic handlersare the only ones that even call it.
R/r2f-constructors.R: thelogical()/integer()/double()/numeric()handlers return one scalar literal (untyped, and integer-typed for the
double fills) carrying array dims;
c()splices the text but sizes theresult from the dims.
array()already solves this with an implied-dospread for fill constructors —
c()never got the same treatment.R/r2f-constructors.R: thematrix()handler'spasses_as_scalar()short-circuit returns the scalar text unchanged with rank-2 dims.
R/r2f-operators-helpers.R:scalarize_matrix()indexes whateverexpression text it is handed, so a cast wrapper becomes unindexable; and
maybe_reshape_vector_matrix()— which only the arithmetic handlers call —applies it for any vector length not provably 1, including symbolic
lengths whose runtime value decides the result shape in R.
Expected behavior
the divisible case) are a compile-time error: "elementwise vector
operations require equal lengths or a scalar operand; R-style recycling is
not supported". Scalar broadcast is unaffected. This applies uniformly to
arithmetic, comparisons,
&/|,%%, and%/%. Implementing trueR recycling would need per-element modulo indexing and, for non-multiple
lengths, a value-dependent warning — poor value for the surface it adds;
an informative refusal keeps
quick(f)honest.symbolic vs constant) get a runtime guard — one scalar
size()comparison per statement through the existing error machinery, raising
the same message as an ordinary R error. Identical symbolic length
expressions stay guard-free. The same policy covers elementwise
matrix-matrix operands axis by axis, and the vector-matrix row rule
(a vector combined column-wise with a matrix whose rows it spans),
which previously refused at compile time whenever the row count could
not be verified statically.
c(numeric(k), x)works and matches R: fills spread as implied-dos(
[(0.0_c_double, i = 1, k), x]) with mode-correct literals.matrix(scalar, m, n)works in any context: it materializes into ahoisted rank-2 temporary where an array is required, and keeps today's
free scalar broadcast when directly assigned.
broadcast when the vector's length is statically known and not 1
(hoisting cast wrappers to a temporary before indexing, so the cells R
supports compile), while comparisons and
&/|treat the 1×1 as anordinary one-row matrix — known longer vectors are a compile error,
unknown lengths get the runtime guard (length 1 still conforms, as in
R). A vector whose length is only known at run time takes the
one-row-matrix rule in arithmetic too, since the result's shape would
otherwise depend on the runtime value: length 1 passes the guard and
yields a 1×1 matrix as in R, longer vectors error where R would
recycle (deprecated).