--- title: "quickr semantics: types, shapes, and differences from R" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{quickr semantics: types, shapes, and differences from R} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` 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. ## What to expect 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. ```{r} library(quickr) ``` ## Types 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](#complex) 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](#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](#integer-sum-overflow)) | | `prod()` | join of the argument types; unlike R, an integer or logical product is integer (see [`prod()`](#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](#ifelse) below) | | BLAS/LAPACK operations such as `%*%`, `crossprod()`, and `solve()` | double for supported real inputs; complex operands are a compile error (see [complex values](#complex)) | | `t(x)`, `diag(x)` | same type as `x`; supported identity forms such as `diag(3L)` are double (see [length-one `diag()`](#length-one-diag) and [scalar `t()`](#scalar-transpose)) | | `cbind()` / `rbind()` | join of all argument types | For example, mixing integer and double promotes: ```{r} f <- quick(function(x, y) { declare(type(x = integer(1)), type(y = integer(1))) x / y }) f(5L, 2L) #> [1] 2.5 ``` and logical arithmetic produces integers, as in R: ```{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](#real-exponents) below. ### Reassignment cannot narrow a type 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: ```{r} 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 double ``` The 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`). ## Shapes 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: ```{r} 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 supported ``` When lengths are not known at compile time, the same rule is enforced at run time: ```{r} 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 supported ``` Scalars and vectors spanning the rows of a matrix can be recycled: ```{r} 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 ``` ### Matrix multiplication and linear algebra `%*%`, `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: ```{r} 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. ## Differences from R 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. ### Integer and logical `prod()` results retain their type {#prod} R 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. ### Integer `sum()` can overflow {#integer-sum-overflow} R 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. ### Floating `sum()` can differ by platform R 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`. ### Integer arithmetic can overflow 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. ### Zero divisors in `%%` 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. ### Floating `%/%` can differ at quotient boundaries For 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. ### Empty `min()` and `max()` inputs are rejected R 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. ### Whole-valued real exponents can differ for negative bases {#real-exponents} 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. ### No `NA` `NA` 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. ### No partial recycling 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. ### Zero-length operands do not recycle to zero-length results 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. ### Some zero-sized linear-algebra outputs are not supported `%*%`, `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 rank The 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. ### A variable's type cannot change `x <- x + 0.5` on an integer `x` is a compile-time error, as described under [Types](#reassignment-cannot-narrow-a-type). 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. ### Empty `sapply()` results R 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. ### Complex values are elementwise-only {#complex} 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: - **Logical-complex pairs.** `+`, `-`, `*`, `==`, 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. - **Type joins.** `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. - **Coercion.** `as.double()` refuses complex input instead of discarding the imaginary parts with a warning as R does. - **Linear algebra.** `%*%`, `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 result Unlike 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 differences Use 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. ### Return allocation and observable effects 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. ### Scalar transpose {#scalar-transpose} 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. ### Length-one `diag()` arguments {#length-one-diag} Literal 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. ### Subscript bounds are only partially checked 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: - Subscripts that are provably out of range at compile time (literal values, ranges provably outside a known extent) are compile errors. Zero and negative literal subscripts are rejected. Read and write subscripts (`x[i]` and `x[i] <- v`) get the same validation. - A subscript whose value is only known at run time — a scalar `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. - Logical masks must have the shape required by the indexed array. Mask extents and scalar logical subscripts are not generally validated; do not rely on R's logical recycling or extension behavior. Validate dynamic indices and mask shapes before entering compiled code. ### Parallel loops require independent iterations `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. ### Local variables and closures 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()` {#ifelse} Differences include: - **Test type.** R coerces numeric `test` values to logical. quickr requires `test` to already be logical and refuses other types. - **Result type.** In R, `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. - **Result shape.** The result takes the shape of `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. - **Branch lengths.** Branches shorter than `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. - **Assignments.** Assignment expressions in `yes` or `no` are not supported. ```{r} quick(function(t, a, b) { declare(type(t = logical(1)), type(a = double(3)), type(b = double(3))) ifelse(t, a, b) }) #> Error: ifelse() result takes the shape of `test`; array-valued yes/no #> with scalar test is not supported ``` ### Logical operators require logical operands {#logical-operators} 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. ### Local closures accept only pure argument expressions 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. ### Deprecation warnings are not reproduced The supported recycling of a `1x1` matrix in arithmetic does not reproduce R's deprecation warning.