quickr compiles a subset of R to Fortran. This vignette describes result types, operand shape checks, and differences to account for when writing compiled functions. It covers the supported numerical operations rather than all of R’s behavior.
For supported inputs, quickr aims to preserve R’s values, types, and shapes. It rejects many unsupported operations at compile time. When an operand shape is only known at run time, supported conformability checks run in the compiled function. Error messages need not match R’s.
Compilation is not a general proof that a function behaves like R. The restrictions and known differences below still apply. In particular, callers must supply valid subscripts and dimensions, avoid missing values and integer overflow, and initialize local variables before they are read. quickr does not perform general definite-assignment or memory-safety analysis.
Value types follow R’s promotion order for the supported atomic modes:
logical < integer < double < complex
Character values are not supported (declaring a
character argument is a compile error). Complex values
support elementwise arithmetic and equality comparisons; order
comparisons (<, <=, >,
>=) and %% are errors, exactly as they are
in R. Outside elementwise operations, complex support is narrower than
the lattice suggests — see complex values under
the differences below. Raw values cannot be used as arithmetic
operands.
An operation’s result type is the “join” of its operands’ types — the highest type among them — except where the table says otherwise:
| Operation | Result type |
|---|---|
+ - * (and unary
+ -) |
join of the operand types; logicals count as integers (in R,
TRUE + TRUE is 2L) |
/ |
double (complex when an operand is complex, as in R) |
^ |
double (complex when an operand is complex, as in R; see note below) |
%%, %/% |
join of the operand types; logicals count as integers |
< <= >
>= == != |
logical |
&, |, !,
&&, || |
logical (see logical operators below) |
c() |
join of all argument types (c(TRUE, FALSE) stays
logical, as in R) |
multi-argument min() / max() /
sum() |
join of all argument types; logicals count as integers (in R,
sum(TRUE, TRUE) is 2L; see integer sum()
overflow) |
prod() |
join of the argument types; unlike R, an integer or logical product
is integer (see prod()) |
single-argument min(), max(), and
sum() |
type of x; logical counts as integer |
abs() |
type of x, except complex input returns double; logical
counts as integer |
ifelse(test, yes, no) |
join of yes and no (but see ifelse below) |
BLAS/LAPACK operations such as %*%,
crossprod(), and solve() |
double for supported real inputs; complex operands are a compile error (see complex values) |
t(x), diag(x) |
same type as x; supported identity forms such as
diag(3L) are double (see length-one diag() and scalar t()) |
cbind() / rbind() |
join of all argument types |
For example, mixing integer and double promotes:
f <- quick(function(x, y) {
declare(type(x = integer(1)), type(y = integer(1)))
x / y
})
f(5L, 2L)
#> [1] 2.5and logical arithmetic produces integers, as in R:
f <- quick(function(x, y) {
declare(type(x = logical(1)), type(y = logical(1)))
x + y
})
r <- f(TRUE, TRUE)
r
#> [1] 2
typeof(r)
#> [1] "integer"A note on ^: the result is double for
real operands, as in R. When the exponent has integer type, quickr uses
an integer exponent, which supports negative bases:
(-2) ^ 3L is -8. A whole-valued
double exponent remains a real exponent; see real exponents below.
R happily re-binds a name to a value of a wider type: after
x <- x + 0.5, an integer x becomes double.
Compiled code cannot re-type a variable, and silently truncating the
value would change results, so quickr makes this a compile-time
error:
quick(function(x) {
declare(type(x = integer(1)))
x <- x + 0.5
x
})
#> Error: cannot reassign `x`: assignment would narrow double to integer;
#> R would promote `x` to doubleThe same check applies to subassignment (x[i] <- ...)
and superassignment (x <<- ...,
x[i] <<- ...). Widening in the other direction
(assigning an integer value to a double variable) is fine and converts
the value to the variable’s fixed type. This matches R for
subassignment. For whole-variable assignment, it is an intentional type
difference: R replaces the binding with the RHS type, whereas quickr
keeps the existing type. For example, x <- 1L with a
double x leaves x double in quickr and makes
it integer in R. The same distinction applies to whole-variable
superassignment (x <<- 1L).
Elementwise arithmetic and comparisons, and the linear-algebra operations below, check operand conformability. Known incompatible dimensions produce compile-time errors. Symbolic dimensions that cannot be proven equal are checked at run time. These checks do not cover every assignment or constructor: keep a binding’s shape stable, and use valid, nonnegative constructor sizes. Do not rely on mismatched section replacements being rejected before a write.
Returned arrays are allocated before the generated function body runs. Use dimensions determined by unchanged inputs or constants. Body-dependent sizes and mutation of variables used in size expressions are not generally tracked safely.
For elementwise operations (arithmetic, comparisons,
&, |), “compatible” means:
| Operands | Behavior |
|---|---|
| scalar with a nonempty array | allowed; the scalar is broadcast |
| identical nonempty shapes | allowed |
vector with matrix, length(vec) == nrow(mat) |
allowed; the vector is recycled column-wise, exactly as in R |
a 1x1 matrix in arithmetic with a vector of
statically known length greater than 1 |
recycled; the result is a plain vector without dims, exactly as in R (which warns that this recycling is deprecated) |
a 1x1 matrix in comparisons and & /
| |
treated as a one-row matrix; mismatched shapes are rejected, and R errors here too |
| anything else — unequal lengths, even when one divides the other | error: R-style recycling is not supported |
A 1x1 matrix meeting a vector whose length is only known
at run time is a case where R’s result shape depends on a
runtime value: R keeps the 1x1 dims when the vector has
length 1 and drops them for any other length. Whether the result has
dimensions must be fixed at compile time, so quickr treats the
1x1 as a one-row matrix there instead: the compiled
function checks that the vector has length 1 and returns a
1x1 matrix — matching R exactly — and errors on a longer
vector, where R would perform the deprecated recycling.
In R, 1:6 + c(10, 20) recycles the shorter vector.
quickr rejects this form of recycling:
quick(function(x, y) {
declare(type(x = double(4)), type(y = double(2)))
x + y
})
#> Error: elementwise vector operations require equal lengths or a scalar
#> operand; R-style recycling is not supportedWhen lengths are not known at compile time, the same rule is enforced at run time:
f <- quick(function(x, y) {
declare(type(x = double(n)), type(y = double(m)))
x + y
})
f(c(1, 2), c(3, 4))
#> [1] 4 6
f(c(1, 2, 3), c(1, 2))
#> Error: elementwise vector operations require equal lengths or a scalar
#> operand; R-style recycling is not supportedScalars and vectors spanning the rows of a matrix can be recycled:
f <- quick(function(x, m) {
declare(type(x = double(3)), type(m = double(3, 2)))
x * m
})
f(c(1, 10, 100), matrix(as.double(1:6), 3, 2))
#> [,1] [,2]
#> [1,] 1 4
#> [2,] 20 50
#> [3,] 300 600%*%, crossprod(),
tcrossprod(), solve(), and the triangular
solvers check conformability like R does. Dimensions known at compile
time are checked at compile time; unknown dimensions are checked at run
time before the underlying BLAS/LAPACK routine is called:
f <- quick(function(a, b) {
declare(type(a = double(n, k)), type(b = double(m, j)))
a %*% b
})
f(matrix(1, 2, 3), matrix(1, 2, 2))
#> Error: non-conformable arguments in %*%solve(a, b) requires a square a, as in R: a
statically rectangular system is a compile error, and when squareness
depends on runtime sizes it is checked before the solve. Use
qr.solve() for least-squares solutions of rectangular
systems. The right-hand side must be a vector or matrix. R also accepts
a scalar right-hand side for a 1x1 system; quickr refuses
that rank-0 form. Declare a length-one vector when that case is
required.
cbind() and rbind() support scalar
recycling and otherwise require the common nonscalar extent to be
provably equal at compile time. Arguments declared with the same
symbolic extent satisfy that requirement. Distinct or otherwise
unresolved extents are a compile error, even if their runtime values
would be equal.
This section describes supported restrictions and known differences. It is not an exhaustive list of implementation defects. Please report unexpected results from supported numerical code.
prod() results retain their
typeR returns a double from prod() when every argument is
integer or logical. quickr currently returns an integer in those cases,
so both the result type and integer-overflow behavior can differ. Use
double inputs, for example prod(as.double(x)), when R’s
double result is required.
sum() can overflowR promotes an integer sum() result to double when the
total exceeds the integer range. quickr accumulates and returns an
integer, so the result can overflow instead. Convert the input to double
before summing when the total may exceed the integer range.
sum() can differ by platformR accumulates double sums in long double on builds where
that type is wider than double. quickr emits a Fortran
sum() over double values instead. With
cancellation-sensitive inputs, the extra precision can change the
result. For example, on an R build with wider long double,
sum(c(1e16, 1, -1e16)) can return 1 in R and
0 in quickr. On platforms without wider
long double precision, both can return 0.
R detects overflow in integer +, -, and
*, warns, and returns NA. quickr does not
support NA and uses Fortran integer arithmetic directly, so
an overflowing result can differ. Convert an operand to double when the
calculation can exceed the integer range.
%% and %/%R can return NA, NaN, or an infinity for a
zero divisor, depending on the operation and operand types. quickr
rejects literal zero divisors in %% and %/%,
but does not check computed divisors. Ensure these divisors are nonzero
before calling compiled code.
%/% can differ at quotient boundariesFor double operands, R corrects a quotient that rounded to an integer
using the corresponding floating-point remainder. quickr floors the
rounded double quotient directly. The results can differ at these
boundaries: 1 %/% 0.2 returns 4 in R and
5 in quickr.
min() and max() inputs are
rejectedR returns Inf from min() and
-Inf from max() when the input is empty.
quickr rejects ordinary array reductions when every input is empty.
Empty operands in a multi-argument call are ignored when another operand
contributes a value. Scalar logical selections are not covered by these
empty-input checks; avoid reductions over such selections.
Empty which.min() and which.max() inputs
also differ. R returns integer(0); quickr returns the
scalar index produced by Fortran’s minloc() or
maxloc(). Do not pass empty inputs to these reductions.
min(), max(), sum(), and
prod() accept only positional data arguments. Named
arguments, including na.rm, are compile errors.
R recognizes a whole-valued double exponent such as 3.0
when raising a negative base. quickr emits a real-to-real power for a
double exponent, which can produce NaN instead of R’s
value. Use an integer exponent when the exponent is integral and the
base can be negative.
NANA values are not supported, in inputs or as literals.
quickr does not implement R’s missing-value semantics, and compiled code
does not detect NA in inputs — passing NA
produces undefined results. (For doubles, NaN and
Inf flow through most arithmetic as in R; double
%/% can return Inf where R returns
NaN, but no guarantees are made for code that branches on
them.)
Comparisons involving NaN do not implement R’s logical
NA result. For example, for a declared double scalar
x = NaN, R evaluates x == x to
NA, while compiled code evaluates it to FALSE.
Equality and ordering comparisons involving NaN, and
logical results derived from them, are unsupported, even when the result
is returned without branching. This applies both to input
NaN values and to those produced by arithmetic inside the
compiled function.
As described above: operand lengths must match exactly or one operand
must be a scalar (or a vector spanning a matrix’s rows). R’s general
recycling rule — including the case where one length divides the other,
which R accepts without a warning — is a compile-time error or a runtime
error instead. This also covers a 1x1 matrix in arithmetic
with a vector whose length is only known at run time: the compiled
function requires length 1 (returning a 1x1 matrix, as R
does) and errors on a longer vector, where R would recycle with a
deprecation warning.
Non-fill array(data, dim) calls also refuse to recycle
data when it is shorter than prod(dim).
One-dimensional arrays are not consistently distinguished from
vectors. Do not rely on preserving their dim attribute
through compiled operations.
matrix(data, nrow, ncol) requires both dimensions and
refuses byrow = TRUE; R can infer an omitted dimension and
supports row-wise filling.
c() accepts scalars and vectors only; it refuses matrix
arguments instead of flattening them as R does.
R arithmetic with a zero-length operand returns a zero-length result
regardless of the other operand’s length: numeric(0) + 1:3
is numeric(0). In quickr, a zero length is a length like
any other. A zero-length non-scalar operand is rejected, including when
the other operand is a scalar or another zero-length vector. This is a
compile error when the zero extent is known and a runtime error when it
is not.
%*%, crossprod(), and
tcrossprod() raise an error when the result would have a
zero extent, where R returns a zero-sized array. A zero contracted
dimension remains supported when every output extent is nonzero, such as
a 2x0 matrix multiplied by a 0x3 matrix; that
result is a 2x3 matrix of zeros, as in R.
outer() and %o% reject zero-sized outputs.
drop() requires a fixed result rankThe rank of a compiled result cannot depend on a runtime extent. When
a matrix axis is not known at compile time, drop() accepts
inputs whose runtime extent is not 1 and raises an error when it is 1.
Declare singleton axes explicitly when drop() should remove
them.
x <- x + 0.5 on an integer x is a
compile-time error, as described under Types. R instead promotes
the variable to double.
Conversion upwards into an existing higher-mode binding is allowed:
assigning 1L to a double x leaves
x double. This is a non-error difference for whole-variable
assignment, since R changes x to integer. Subassignment
retains the higher type in both R and quickr.
sapply() resultsR returns list() for an empty sapply()
input. quickr’s compiled output has a fixed atomic type, so it cannot
reproduce that result. Use nonempty inputs; an empty input is not
consistently rejected.
Elementwise arithmetic and equality comparisons handle complex values
natively, including mixed integer, double, and complex operands:
complex + double promotes to complex, as in R. Restrictions
include:
+,
-, *, ==, and != do
not promote a logical operand to complex, so those pairs are compile
errors where R returns a complex arithmetic or logical equality result.
/ and ^ do support logical-complex pairs.c(),
ifelse(), and cbind()/rbind() do
not mix complex with other types — c(x, y) with a complex
x and a double y is a compile error where R
returns a complex vector.as.double() refuses complex
input instead of discarding the imaginary parts with a warning as R
does.%*%,
crossprod(), solve(), and chol()
refuse complex operands at compile time; these lowerings are
double-only. R supports complex operands for %*%,
crossprod(), and solve(), but not for every
linear-algebra routine.(t(x) and diag(x) are mode-preserving and
keep working on complex values; order comparisons and %% on
complex values are errors in R itself, so refusing them is not a
divergence.)
solve() uses LAPACK’s singularity resultUnlike R, solve() does not apply a reciprocal-condition
tolerance. It only reports an exact LAPACK singularity, so an
ill-conditioned system may return a result that R rejects as
computationally singular.
runif() bound differencesUse a nonnegative scalar count and scalar bounds with
runif(). R also accepts a vector count and recycled vector
bounds; these forms are not supported reliably by quickr and are not
consistently rejected.
R returns NaN values with a warning when
runif() is called with min > max. quickr
applies its uniform scaling formula without checking the bound order and
returns values between the two bounds. Check the bounds before calling
runif() when their order is not known statically.
When min and max are equal, R returns the
bound without consuming an RNG value. quickr still draws and scales a
uniform value, so the result is equal but the subsequent RNG state
differs.
Keep RNG calls out of parallel regions. Direct runif()
calls in parallel loops are rejected, but calls through nested local
closures are not generally tracked for this purpose.
C allocates returned arrays before the generated Fortran body runs. Operation-specific checks, such as matrix conformability, execute in Fortran before the native operation. An allocation error can therefore occur before an operation reports an invalid shape. quickr does not impose a memory budget.
Dimension arithmetic is not generally protected from overflow. Use sizes whose intermediate expressions and final array lengths are representable. On failing calls, compilation or allocation can also prevent prints or random draws that R would have performed. quickr does not promise the same first error or the same partial effects as R.
R’s t(1) returns a 1x1 matrix. quickr
returns the literal scalar without dimensions. Transposing an argument
declared as a length-one vector, such as double(1), can
instead fail during native compilation. These scalar cases are separate
from the supported vector and matrix transpose forms.
diag() argumentsLiteral identity forms such as diag(3L) produce a double
identity matrix. An argument declared as a length-one vector instead
follows the diagonal-value form: diag(n) for
n = 3L declared as integer(1) currently
returns an integer 1x1 matrix containing 3,
where R returns a 3x3 identity matrix. Use an explicit
identity form such as diag(nrow = n) when the size is an
argument.
R validates subscripts: x[0] drops elements, negative
subscripts exclude, and out-of-range reads pad with NA.
quickr’s checks cover only part of that behavior:
x[i] and x[i] <- v) get the same
validation.x[i], or a range x[a:b] with symbolic bounds —
is not checked, matching Fortran’s own contract:
reading or writing out of bounds is undefined behavior, as in C. It will
not return NA-padded results the way R does.Validate dynamic indices and mask shapes before entering compiled code.
declare(parallel()) requests OpenMP execution. Each
iteration must be independent: it must not update shared state that
another iteration reads or writes. For example,
x <- x + 1L on an enclosing binding is not a supported
parallel reduction. It can race or have different binding behavior from
R. Automatic detection of these writes and explicit reduction semantics
remain separate work. Use serial execution for dependent iterations, and
do not rely on the final values of private loop locals after a parallel
region.
Initialize local variables on every path before reading them. Local closures use generated procedures and host association; general R environment lookup, runtime rebinding of function names, and mutation-sensitive argument/default evaluation are not fully implemented. Prefer distinct local names, simple captures, and arguments that do not depend on state changed by the callee.
ifelse()Differences include:
test
values to logical. quickr requires test to already be
logical and refuses other types.ifelse()’s result
type can depend on which branches are actually reached at run time:
ifelse(c(TRUE, TRUE), 1:2, c(0.5, 0.5)) is integer, because
the no branch never materializes. quickr always promotes to
the join of both branch types (double here), regardless of
test’s values.test, as in R. A scalar test with array-valued
branches (where R would return a length-1 result sliced out of the
array) is a compile error.test are recycled by R; quickr requires each branch to be a
scalar or match test’s length (checked at run time when
lengths are unknown). A branch known to be zero-length is a compile
error.yes or no are not supported.R coerces numeric operands to logical (1 & 2 is
TRUE and !1 is FALSE); quickr
requires the operands of !, &,
|, &&, and || to already
be logical and errors otherwise.
any() and all() likewise require logical
operands instead of coercing numeric inputs as R does.
&& and || otherwise follow R’s
scalar semantics: operands must be length 1 (a compile error when the
length is known, or a runtime error when it is not), and they
short-circuit — the right operand is evaluated only when the left side
does not decide the answer, so a guarded access like
i >= 1L && i <= n && x[i] > 0
avoids reading outside a vector of length n. Use
& and | for vectors; assignment
expressions in && and || are not
supported.
R evaluates function arguments lazily. Local closures inside compiled
functions (including functions passed to sapply()) accept
only argument expressions that quickr can prove are pure, and evaluate
accepted arguments eagerly. Calls with effectful expressions such as
pair(runif(1), 2) are compile errors. Code that relies on
an argument never being evaluated will behave differently.
The supported recycling of a 1x1 matrix in arithmetic
does not reproduce R’s deprecation warning.