diff --git a/DESCRIPTION b/DESCRIPTION index dbefb6a..7c8c4da 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,10 +23,13 @@ Imports: Suggests: bench, cli, + knitr, pkgload (>= 1.4.0), rlang, + rmarkdown, testthat (>= 3.0.0), withr +VignetteBuilder: knitr Config/testthat/edition: 3 Config/testthat/parallel: true Config/testthat/start-first: unary-intrinsics, loops diff --git a/NEWS.md b/NEWS.md index b27319a..94ff236 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,85 @@ * Successful flang availability checks are now reused for the rest of the R session. Restart R after changing the flang toolchain. +- `c()` now accepts matrix and array arguments, flattening them in + column-major order like R (previously `c(m)` for a matrix `m` errored + with "all args passed to c() must be scalars or 1-d arrays"). `as.vector()` + is now supported: it drops dimensions (mode `"any"`, the default, + preserves the type; `mode = "double"`/`"integer"`/`"numeric"` coerce). + +- Reassigning a variable now requires the new value's shape to be + compatible with the declared shape, extending the existing rank check + to every dimension: quickr cannot re-declare a Fortran variable the + way R rebinds a symbol. A statically known mismatch (e.g. + `x <- numeric(2); x <- numeric(3)`) is a compile-time error; + dimensions that cannot be compared at compile time are checked at run + time. Previously such reassignments silently kept the old shape or + produced invalid Fortran. This also covers reassignment between a scalar + and an array in either direction: `x <- numeric(n); x <- 0` used to + broadcast the scalar across every element, and assigning an array to a + scalar variable used to keep only its first element, where R rebinds the + symbol in both cases. Locals declared with unknown (`NA`) dims still + reallocate on assignment, like R. + +- `diag(x)` with a length-1 `x` and no `nrow`/`ncol` now builds the + identity matrix of size `x`, matching R. Previously only a *literal* + size took that path, so `diag(n)` with `n` declared `integer(1)` + returned a 1x1 matrix containing `n` instead of the n-by-n identity — + a silent divergence in both shape and values. As in R the size is + `as.integer(x)`, so a `double` or `logical` `x` works and truncates + toward zero (`diag(3.7)` is the 3x3 identity, as in R). Use + `diag(x, nrow)` for a 1x1 matrix holding `x`. + +- `declare()` size expressions now accept `as.integer()`, which lowers to + Fortran's `INT()` and to an integer cast in the generated C bridge. + +- Elementwise operations (arithmetic, comparisons, `&`, `|`) now require + operand lengths to match, unless one operand is a scalar or a vector is + combined column-wise with a matrix whose rows it spans. R-style partial + recycling was never implemented: expressions like `x + y` with + `length(x) == 4`, `length(y) == 2` previously compiled to code that read + out of bounds. Statically unequal lengths are now a compile-time error; + lengths that cannot be verified at compile time are checked at run time. + +- `1x1`-matrix operands now follow R's rules: in arithmetic against a + vector of statically known length other than 1 they are treated as + scalars and the result is a plain vector (which R allows, with a + deprecation warning), while in comparisons and `&`/`|` they are treated + as one-row matrices, so mismatched shapes are rejected — matching R, + which raises an error. When the vector's length is only known at run + time, the result's shape would depend on that value (R keeps the `1x1` + dims for a length-1 vector and drops them otherwise), so arithmetic + also takes the one-row-matrix rule: a runtime check requires length 1 + and the result is a `1x1` matrix; longer vectors raise an error where + R would recycle. Previously a `1x1` matrix was scalarized in arithmetic + and not shape-checked at all in comparisons and `&`/`|`, so e.g. + `x < m` with `m` a `1x1` matrix failed to build with a Fortran rank + mismatch instead of a quickr error, an operand needing a cast failed to + build even in arithmetic, and a symbolic-length `x + m` returned a plain + vector where R returns a `1x1` matrix. + +- `&&` and `||` now behave like R's scalar control operators. They + require length-1 logical operands (longer operands are a compile-time + error, as they are a runtime error in R; use `&`/`|` for elementwise + logic), and they short-circuit: the right operand is evaluated only when + the left side does not decide the answer, so idioms like + `while (i <= n && x[i] > 0)` are safe. Previously they compiled exactly + like `&`/`|` — elementwise over vectors (returning answers where R + errors) and with both sides always evaluated. + +- `solve(a, b)` now requires a square `a`, matching R. A rectangular `a` + previously fell through to a least-squares solve (dgels), returning an + answer where R raises `'a' (m x n) must be square`. Statically + rectangular systems are now a compile-time error; when squareness is not + known at compile time it is checked at run time. Use `qr.solve()` for + least-squares solutions of rectangular systems (unchanged). + +- Complex operands in linear algebra (`%*%`, `crossprod()`, `solve()`, + `chol()`, ...) are now a compile-time error. quickr's lowerings use the + real (double-only) BLAS/LAPACK routines, which previously read complex + storage as reals and returned a plausible but wrong real result where R + returns a complex one. Elementwise complex arithmetic and the + mode-preserving `t()`/`diag()` are unaffected. # quickr 0.3.0 diff --git a/R/aaa-utils.R b/R/aaa-utils.R index 3c806dc..502112d 100644 --- a/R/aaa-utils.R +++ b/R/aaa-utils.R @@ -274,16 +274,6 @@ str_flatten_commas <- function(...) { paste0(unlist(c(character(), ...), use.names = FALSE), collapse = ", ") } -str_flatten_args <- function(..., multiline = length(dots) >= 3) { - dots <- unlist(c(character(), ...), use.names = FALSE) - if (multiline) { - dots <- paste0("\n ", dots, collapse = ",") - paste(dots, "\n") - } else { - paste0(dots, collapse = ",") - } -} - interleave <- function(x, y) { stopifnot(is.atomic(x), is.atomic(y), length(y) == 1L, typeof(x) == typeof(y)) drop_last(as.vector(rbind(x, y, deparse.level = 0L))) diff --git a/R/c-wrapper.R b/R/c-wrapper.R index a1892e5..8d6b614 100644 --- a/R/c-wrapper.R +++ b/R/c-wrapper.R @@ -163,6 +163,8 @@ make_c_bridge <- function( c_args <- paste("SEXP", names(formals(closure)), collapse = ", ") needs_rmath <- any(grepl("R_pow(", c_body, fixed = TRUE)) + needs_math <- any(grepl("floor(", c_body, fixed = TRUE)) || + any(grepl("fmod(", c_body, fixed = TRUE)) c_body <- as_glue(str_flatten_lines(c_body)) c_func_def <- glue("SEXP {fsub@name}_(SEXP _args) {c_block(c_body)}") @@ -171,6 +173,7 @@ make_c_bridge <- function( c_headers <- str_flatten_lines( "#define R_NO_REMAP", + if (needs_math) "#include ", "#include ", "#include ", if (needs_rmath || isTRUE(force_rmath_header)) "#include ", @@ -480,8 +483,16 @@ c_bridge_hoist_seq_checks <- function(hoist, from, to, by) { } -as_c_name <- function(var, c_hoist = NULL) { +as_c_name <- function(var, c_hoist = NULL, preserve_numeric = FALSE) { stopifnot(inherits(var, Variable)) + if (isTRUE(preserve_numeric)) { + if (identical(var@mode, "double")) { + return(glue("Rf_asReal({var@name})")) + } + if (!var@mode %in% c("integer", "logical")) { + stop("unsupported numeric size expression mode: ", var@mode) + } + } expr <- glue("Rf_asInteger({var@name})") if (is.null(c_hoist)) { return(expr) @@ -531,13 +542,22 @@ dims2c_dim_index_expr <- function(cl, scope) { get_size_name(var, axis) } -dims2c_expr <- function(e, scope, c_hoist = NULL) { +dims2c_expr <- function( + e, + scope, + c_hoist = NULL, + preserve_numeric = FALSE +) { if (is.null(e)) { return(NULL) } if (inherits(e, Variable)) { - return(as_c_name(e, c_hoist = c_hoist)) + return(as_c_name( + e, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + )) } if (is_scalar_integer(e)) { @@ -560,7 +580,11 @@ dims2c_expr <- function(e, scope, c_hoist = NULL) { if (!inherits(var, Variable)) { stop("could not resolve size: ", nm) } - return(as_c_name(var, c_hoist = c_hoist)) + return(as_c_name( + var, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + )) } if (!is.call(e)) { @@ -574,7 +598,12 @@ dims2c_expr <- function(e, scope, c_hoist = NULL) { if (length(args) != 1L) { stop("unsupported size expression: ", deparse1(e)) } - return(dims2c_expr(args[[1L]], scope, c_hoist = c_hoist)) + return(dims2c_expr( + args[[1L]], + scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + )) } if (identical(op, "length")) { @@ -623,37 +652,88 @@ dims2c_expr <- function(e, scope, c_hoist = NULL) { if (length(args) != 1L) { stop("abs() expects one argument") } - e1 <- dims2c_expr(args[[1L]], scope, c_hoist = c_hoist) + e1 <- dims2c_expr( + args[[1L]], + scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + ) return(glue("(({e1}) < 0 ? -({e1}) : ({e1}))")) } + if (identical(op, "as.integer")) { + if (length(args) != 1L) { + stop("as.integer() expects one argument") + } + # a C cast to an integer type truncates toward zero, as R's + # as.integer() does + e1 <- dims2c_expr( + args[[1L]], + scope, + c_hoist = c_hoist, + preserve_numeric = TRUE + ) + return(glue("((R_xlen_t)({e1}))")) + } + if (op %in% c("+", "-", "*", "/", "%/%", "%%", "^")) { if (length(args) == 1L && op %in% c("+", "-")) { - e1 <- dims2c_expr(args[[1L]], scope, c_hoist = c_hoist) + e1 <- dims2c_expr( + args[[1L]], + scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + ) return(glue("({op}({e1}))")) } if (length(args) != 2L) { stop("unsupported size expression: ", deparse1(e)) } - e1 <- dims2c_expr(args[[1L]], scope, c_hoist = c_hoist) - e2 <- dims2c_expr(args[[2L]], scope, c_hoist = c_hoist) + e1 <- dims2c_expr( + args[[1L]], + scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + ) + e2 <- dims2c_expr( + args[[2L]], + scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + ) return(switch( op, `+` = glue("({e1} + {e2})"), `-` = glue("({e1} - {e2})"), `*` = glue("({e1} * {e2})"), `/` = glue("((double)({e1}) / (double)({e2}))"), - `%/%` = glue("((R_xlen_t){e1} / (R_xlen_t){e2})"), - `%%` = glue("((R_xlen_t){e1} % (R_xlen_t){e2})"), + `%/%` = glue("floor((double)({e1}) / (double)({e2}))"), + `%%` = glue( + "fmod(fmod((double)({e1}), (double)({e2})) + ", + "(double)({e2}), (double)({e2}))" + ), `^` = glue("R_pow((double)({e1}), (double)({e2}))") )) } if (op %in% c("min", "max")) { if (!length(args)) { - return("0") + stop( + op, + "() size expressions require at least one argument", + call. = FALSE + ) + } + rendered <- lapply( + args, + dims2c_expr, + scope = scope, + c_hoist = c_hoist, + preserve_numeric = preserve_numeric + ) + if (length(rendered) == 1L) { + return(rendered[[1L]]) } - rendered <- lapply(args, dims2c_expr, scope = scope, c_hoist = c_hoist) cmp <- if (identical(op, "min")) "<" else ">" reduce(rendered, \(a, b) glue("(({a}) {cmp} ({b}) ? ({a}) : ({b}))")) } else { @@ -665,7 +745,13 @@ dims2c <- function(dims, scope, c_hoist = NULL) { if (!length(dims) || identical(dims, list(1L))) { return(list(NULL, "1")) } - lapply(dims, dims2c_expr, scope = scope, c_hoist = c_hoist) + lapply( + dims, + dims2c_expr, + scope = scope, + c_hoist = c_hoist, + preserve_numeric = TRUE + ) } c_dims2c_len <- function(c_dims) { @@ -837,9 +923,13 @@ fsub_extern_decl <- function(fsub) { glue("{fsub_arg_var_c_type(var)} {var@name}__") } }) - if (length(fsub_c_sig) >= 3L) { - fsub_c_sig <- paste0("\n ", fsub_c_sig) + args_sig <- if (length(fsub_c_sig) >= 3L) { + # one arg per line; join with a bare comma -- joining "\n "-prefixed + # elements with ", " leaves a trailing space on every line + paste0("\n ", fsub_c_sig, collapse = ",") + } else { + str_flatten_commas(fsub_c_sig) } - glue("extern void {fsub@name}({str_flatten_commas(fsub_c_sig)});") + glue("extern void {fsub@name}({args_sig});") } diff --git a/R/classes.R b/R/classes.R index cb6fb76..f4b6f2a 100644 --- a/R/classes.R +++ b/R/classes.R @@ -4,10 +4,9 @@ NULL new_setter <- function( coerce = NULL, coerce_null = FALSE, - set_once = FALSE, env = parent.frame(2L) ) { - if (is.null(coerce) || isFALSE(coerce) && isFALSE(set_once)) { + if (is.null(coerce) || isFALSE(coerce)) { return() } @@ -15,18 +14,8 @@ new_setter <- function( name <- as.character(last(attr(self, ".setting_prop", TRUE))) ) - check_set_once <- if (set_once) { - quote( - if (!is.null(prop(self, name))) { - stop(name, " can only be set once") - } - ) - } - rebind_coerced_value <- - if (is.null(coerce) || isFALSE(coerce)) { - NULL - } else if (isTRUE(coerce)) { + if (isTRUE(coerce)) { quote( value <- convert( from = value, @@ -57,7 +46,6 @@ new_setter <- function( body = as.call(c( quote(`{`), bind_name, - check_set_once, rebind_coerced_value, set )), @@ -68,14 +56,12 @@ new_setter <- function( new_scalar_validator <- function( allow_null = FALSE, - allow_na = FALSE, - additional_checks = NULL, - env = parent.frame(2L) + additional_checks = NULL ) { checks <- c( if (allow_null) quote(if (is.null(value)) return()), quote(if (length(value) != 1L) return("must be a scalar")), - if (!allow_na) quote(if (anyNA(value)) return("must not be NA")), + quote(if (anyNA(value)) return("must not be NA")), additional_checks ) @@ -89,19 +75,13 @@ new_scalar_validator <- function( prop_bool <- function( default, - allow_null = FALSE, - allow_na = FALSE, - set_once = FALSE + allow_null = FALSE ) { - stopifnot(is_bool(set_once), is_bool(allow_null), is_bool(allow_na)) + stopifnot(is_bool(allow_null)) new_property( class = if (allow_null) NULL | class_logical else class_logical, - setter = new_setter(set_once = set_once), - validator = new_scalar_validator( - allow_null = allow_null, - allow_na = allow_na - ), + validator = new_scalar_validator(allow_null = allow_null), default = default ) } @@ -110,11 +90,9 @@ prop_bool <- function( prop_string <- function( default = NULL, allow_null = FALSE, - allow_na = FALSE, - coerce = FALSE, - set_once = FALSE + coerce = FALSE ) { - stopifnot(is_bool(set_once), is_bool(allow_null), is_bool(allow_na)) + stopifnot(is_bool(allow_null)) if (isTRUE(coerce)) { coerce <- quote(as.character) @@ -126,8 +104,7 @@ prop_string <- function( validator = new_scalar_validator(allow_null = allow_null), setter = new_setter( coerce = coerce, - coerce_null = !allow_null, - set_once = set_once + coerce_null = !allow_null ) ) } @@ -136,11 +113,9 @@ prop_string <- function( prop_wholenumber <- function( default = NULL, allow_null = FALSE, - allow_na = FALSE, - coerce = TRUE, - set_once = FALSE + coerce = TRUE ) { - stopifnot(is_bool(set_once), is_bool(allow_null), is_bool(allow_na)) + stopifnot(is_bool(allow_null)) if (isTRUE(coerce)) { coerce <- quote( @@ -157,8 +132,7 @@ prop_wholenumber <- function( default = as.integer(default), setter = new_setter( coerce = coerce, - coerce_null = !allow_null, - set_once = set_once + coerce_null = !allow_null ), validator = new_scalar_validator(allow_null = allow_null) ) @@ -169,8 +143,7 @@ prop_enum <- function( values, nullable = FALSE, default = if (nullable) NULL else values[1], - exact = FALSE, - set_once = FALSE + exact = FALSE ) { stopifnot( "values must be a character vector of length >= 2 without any NA" = is.character( @@ -211,8 +184,7 @@ prop_enum <- function( class = if (nullable) NULL | class_character else class_character, setter = new_setter( coerce = coerce, - coerce_null = !nullable, - set_once = set_once + coerce_null = !nullable ), validator = validator, default = default @@ -233,7 +205,7 @@ prop_enum <- function( # the print method for this should only print non-null values Variable := new_class( properties = list( - mode = prop_enum(.atomic_type_names, nullable = TRUE, set_once = FALSE), + mode = prop_enum(.atomic_type_names, nullable = TRUE), dims = new_property( # NULL means scalar @@ -282,8 +254,7 @@ Variable := new_class( typeof(value), symbol = as.character(value), value - )), - set_once = FALSE #TRUE + )) ), r_name = prop_string( @@ -292,8 +263,7 @@ Variable := new_class( typeof(value), symbol = as.character(value), value - )), - set_once = FALSE + )) ), rank = new_property( @@ -339,6 +309,14 @@ Variable := new_class( # storage (0/1) rather than Fortran LOGICAL. logical_as_int = prop_bool(default = FALSE), + # Fortran kind for integer variables. User-facing R integers remain c_int; + # pointer-sized compiler locals opt into c_ptrdiff_t explicitly. + integer_kind = prop_enum( + c("c_int", "c_ptrdiff_t"), + default = "c_int", + exact = TRUE + ), + # TRUE when the variable is available via host association and should not # be redeclared in the local scope. host_associated = prop_bool(default = FALSE), @@ -350,7 +328,13 @@ Variable := new_class( validator = function(self) { if (isTRUE(self@logical_as_int) && !identical(self@mode, "logical")) { - "`logical_as_int` can only be TRUE when `mode` is 'logical'" + return("`logical_as_int` can only be TRUE when `mode` is 'logical'") + } + if ( + !identical(self@integer_kind, "c_int") && + !identical(self@mode, "integer") + ) { + "`integer_kind` can only differ from 'c_int' when `mode` is 'integer'" } } ) @@ -426,6 +410,9 @@ R2FHandler := new_class( dest_supported = prop_bool(default = FALSE), dest_infer = new_property(NULL | class_function), dest_infer_name = prop_string(default = NULL, allow_null = TRUE), + # Set when the handler was registered as a namespace-level named function, + # so dispatch can re-resolve it by name. See register_r2f_handler(). + fun_name = prop_string(default = NULL, allow_null = TRUE), # When NULL, r2f will resolve the callable by name and use match.call(). # When FALSE, r2f will not attempt match.call(). match_fun = new_property( diff --git a/R/manifest.R b/R/manifest.R index f80e5f1..cd3d2ba 100644 --- a/R/manifest.R +++ b/R/manifest.R @@ -39,6 +39,21 @@ logical_as_int <- function(var) { identical(var@mode, "logical") && isTRUE(var@logical_as_int) } +# A mode with no Fortran translation reached the code generator. Declared +# modes are validated at declare() time; this is the backstop for values +# created mid-translation, so it must still read as a user-facing message, +# not an internal object dump. +stop_unsupported_mode <- function(var) { + name <- var@r_name %||% var@name + stop( + if (is.null(name)) "" else paste0("variable `", name, "`: "), + "mode '", + var@mode %||% "?", + "' is not supported by quickr", + call. = FALSE + ) +} + block_tmp_allocatable_threshold <- 16L subroutine_local_allocatable_threshold_bytes <- 256L * 1024L @@ -73,7 +88,11 @@ var_storage_bytes <- function(var) { switch( var@mode, double = 8, - integer = 4, + integer = if (identical(var@integer_kind, "c_ptrdiff_t")) { + .Machine$sizeof.pointer + } else { + 4 + }, complex = 16, logical = 4, raw = 1, @@ -94,21 +113,7 @@ subroutine_local_allocatable <- function( # For declarations like `type(a = double(NA, NA))`, substitute_declared_sizes() # rewrites NA axes to `a__dim_*` symbols. Those sizes are not available for # explicit allocation, so treat these as implicitly-sized locals. - self_size_names <- vapply( - seq_along(var@dims), - function(i) get_size_name(var, axis = as.integer(i)), - character(1) - ) - if ( - any(vapply( - seq_along(var@dims), - function(i) { - d <- var@dims[[i]] - is.symbol(d) && identical(as.character(d), self_size_names[[i]]) - }, - logical(1) - )) - ) { + if (has_self_size_dims(var)) { return(FALSE) } @@ -196,11 +201,11 @@ iso_c_binding_symbols <- function( switch( var@mode, double = "c_double", - integer = "c_int", + integer = var@integer_kind, complex = "c_double_complex", logical = if (isTRUE(logical_is_c_int(var))) "c_int", raw = "c_int8_t", - stop("unrecognized kind: ", format(var)) + stop_unsupported_mode(var) ), lapply(var@dims, function(size) { syms <- all.vars(size) @@ -262,11 +267,11 @@ emit_decl_line <- function( type <- switch( var@mode, double = "real(c_double)", - integer = "integer(c_int)", + integer = glue("integer({var@integer_kind})"), complex = "complex(c_double_complex)", logical = if (logical_as_int(var)) "integer(c_int)" else "logical", raw = "integer(c_int8_t)", - stop("unrecognized kind: ", format(var)) + stop_unsupported_mode(var) ) # Block-scoped temporaries are explicitly marked allocatable so we can @@ -371,11 +376,11 @@ r2f.scope <- function(scope, include_errors = FALSE) { type <- switch( var@mode, double = "real(c_double)", - integer = "integer(c_int)", + integer = glue("integer({var@integer_kind})"), complex = "complex(c_double_complex)", logical = if (logical_as_int(var)) "integer(c_int)" else "logical", raw = "integer(c_int8_t)", - stop("unrecognized kind: ", format(var)) + stop_unsupported_mode(var) ) dims <- if (passes_as_scalar(var)) { @@ -386,19 +391,7 @@ r2f.scope <- function(scope, include_errors = FALSE) { # In subroutines, locals declared with unspecified dims (NA -> `a__dim_*`) # are emitted as deferred-shape allocatables and rely on implicit allocation. - if ( - is.null(intent) && - !is.null(dims) && - any(vapply( - seq_along(var@dims), - function(i) { - d <- var@dims[[i]] - is.symbol(d) && - identical(as.character(d), get_size_name(var, axis = i)) - }, - logical(1) - )) - ) { + if (is.null(intent) && !is.null(dims) && has_self_size_dims(var)) { dims <- sprintf("(%s)", str_flatten_commas(rep(":", var@rank))) } @@ -514,14 +507,28 @@ dims2f_eval_base_env[["("]] <- baseenv()[["("]] dims2f_eval_base_env[["+"]] <- function(e1, e2) glue("({e1} + {e2})") dims2f_eval_base_env[["-"]] <- function(e1, e2) glue("({e1} - {e2})") dims2f_eval_base_env[["*"]] <- function(e1, e2) glue("({e1} * {e2})") -dims2f_eval_base_env[["/"]] <- function(e1, e2) glue("real({e1}) / real({e2})") -# dividing integers truncates towards 0 -dims2f_eval_base_env[["%/%"]] <- function(e1, e2) glue("int({e1}) / int({e2})") +dims2f_eval_base_env[["/"]] <- function(e1, e2) { + glue("real({e1}, kind=c_double) / real({e2}, kind=c_double)") +} +dims2f_eval_base_env[["%/%"]] <- function(e1, e2) { + quotient <- glue( + "(real({e1}, kind=c_double) / real({e2}, kind=c_double))" + ) + real_floor_expr(quotient) +} dims2f_eval_base_env[["%%"]] <- function(e1, e2) { - glue("mod(int({e1}), int({e2}))") + glue( + "modulo(real({e1}, kind=c_double), real({e2}, kind=c_double))" + ) +} +dims2f_eval_base_env[["^"]] <- function(e1, e2) { + glue("(real({e1}, kind=c_double))**({e2})") } -dims2f_eval_base_env[["^"]] <- function(e1, e2) glue("({e1})**({e2})") dims2f_eval_base_env[["abs"]] <- function(x) glue("abs({x})") +# Fortran INT() truncates toward zero, like as.integer() in R. +dims2f_eval_base_env[["as.integer"]] <- function(x) { + glue("int({x}, kind=c_ptrdiff_t)") +} dims2f_eval_base_env[["quickr_seq_length"]] <- function(from, to, by) { safe_by <- glue("merge(int({by}), 1, int({by}) /= 0)") glue("(abs((int({to}) - int({from})) / {safe_by}) + 1)") @@ -558,15 +565,50 @@ dims2f_eval_base_env[["["]] <- function(x, i) { glue("size({x}, {as.integer(i)})") } } +# Fortran min()/max() require operands of one type and kind. Normalize their +# operands to c_double, then apply the single final extent cast in dims2f(). dims2f_eval_base_env[["min"]] <- function(...) { args <- list(...) + if (!length(args)) { + stop("min() size expressions require at least one argument", call. = FALSE) + } + args <- map_chr( + args, + \(arg) glue("real({arg}, kind=c_double)") + ) + if (length(args) == 1L) { + return(args[[1L]]) + } glue("min({str_flatten_commas(args)})") } dims2f_eval_base_env[["max"]] <- function(...) { args <- list(...) + if (!length(args)) { + stop("max() size expressions require at least one argument", call. = FALSE) + } + args <- map_chr( + args, + \(arg) glue("real({arg}, kind=c_double)") + ) + if (length(args) == 1L) { + return(args[[1L]]) + } glue("max({str_flatten_commas(args)})") } +dims2f_needs_final_size_cast <- function(e) { + if (!is.call(e)) { + return(FALSE) + } + if ( + as.character(e[[1L]]) %in% + c("/", "%/%", "%%", "^", "as.integer", "min", "max") + ) { + return(TRUE) + } + any(vapply(as.list(e)[-1L], dims2f_needs_final_size_cast, logical(1))) +} + dims2f <- function(dims, scope) { syms <- unique(unlist(lapply(dims, \(d) if (is.language(d)) all.vars(d)))) @@ -576,15 +618,16 @@ dims2f <- function(dims, scope) { names(vars) <- syms eval_env <- list2env(vars, parent = dims2f_eval_base_env) dims <- map_chr(dims, function(d) { + original <- d d <- eval(d, eval_env) if (is.symbol(d)) { - as.character(d) + d <- as.character(d) } else if (is_wholenumber(d)) { - as.character(d) + d <- as.character(d) } else if (is_scalar_na(d)) { - ":" + return(":") } else if (is_string(d)) { - d + d <- d } else if (inherits(d, Variable)) { # a locally allocated var that is a return var if (!d@modified && d@is_arg) { @@ -592,6 +635,10 @@ dims2f <- function(dims, scope) { } stop("unexpected axis size value") } + if (dims2f_needs_final_size_cast(original)) { + d <- glue("int(({d}), kind=c_ptrdiff_t)") + } + d }) if (!length(dims) || identical(dims, "1")) { "" diff --git a/R/r2f-aaa-registry.R b/R/r2f-aaa-registry.R index 04a71f0..c3d4efb 100644 --- a/R/r2f-aaa-registry.R +++ b/R/r2f-aaa-registry.R @@ -14,6 +14,15 @@ register_r2f_handler <- function( handler <- if (inherits(fun, R2FHandler)) fun else R2FHandler(fun) + # Same hazard as `dest_infer` below, for the handler itself: the function + # object is captured here, at build time, while covr rebinds its instrumented + # copies into the namespace after the package has loaded. A handler registered + # as a top-level named function would keep dispatching the copy taken here and + # read as 0% covered however well it is tested. Record the name so + # get_r2f_handler() can re-resolve it; the object stays authoritative, since + # most handlers are anonymous literals with no name to resolve. + handler@fun_name <- registered_fun_name(substitute(fun), fun) + if (!is.null(dest_supported)) { handler@dest_supported <- isTRUE(dest_supported) } @@ -47,3 +56,28 @@ register_r2f_handler <- function( } invisible(handler) } + + +# The name to re-resolve a registered handler by, or NULL if there isn't one. +# +# `expr` is the unevaluated `fun` argument and `fun` its value. A name is only +# usable if `expr` is a symbol *and* it names this same function in a namespace +# -- the only environment covr rebinds into. That rules out the two non-literal +# registrations that are not namespace-level functions: the local `handler` +# closure built by register_unary_intrinsic() (a symbol, but bound in a call +# frame, where the name would mean something else on a later call) and +# `r2f_handlers[["<-"]]` (not a symbol at all). +registered_fun_name <- function(expr, fun) { + if (!is.symbol(expr)) { + return(NULL) + } + name <- as.character(expr) + env <- environment(fun) + if (!is.environment(env) || !isNamespace(env)) { + return(NULL) + } + if (!identical(get0(name, envir = env, mode = "function"), fun)) { + return(NULL) + } + name +} diff --git a/R/r2f-aab-core.R b/R/r2f-aab-core.R index d462e44..a658308 100644 --- a/R/r2f-aab-core.R +++ b/R/r2f-aab-core.R @@ -20,6 +20,10 @@ new_hoist <- function(scope) { has_block <- function() !is.null(block_scope) + # TRUE when render(code) would return `code` unchanged: nothing emitted, + # no block-scoped temporaries declared. + is_empty <- function() !length(hoisted) && !has_block() + ensure_block_scope <- function() { if (is.null(block_scope)) { block_scope <<- scope_new_child(scope, "block") @@ -42,7 +46,7 @@ new_hoist <- function(scope) { render <- function(code) { code <- str_split_lines(code) - if (!length(hoisted) && !has_block()) { + if (is_empty()) { return(str_flatten_lines(code)) } @@ -65,30 +69,66 @@ new_hoist <- function(scope) { list( emit = emit, declare_tmp = declare_tmp, + is_empty = is_empty, render = render ), parent = emptyenv() ) } +# Materialize `code` into a hoisted temporary and return the temporary. +# `hoist` is always available in a handler: r2f() opens one per statement +# before dispatching, and every caller forwards the one it received. +# Used by: hoist_unless_name(), r2f-constructors.R, r2f-subscript.R, +# r2f-rev.R +materialize_via_hoist <- function( + code, + mode, + dims, + hoist, + logical_as_int = FALSE +) { + stopifnot(is.environment(hoist)) + tmp <- hoist$declare_tmp( + mode = mode, + dims = dims, + logical_as_int = logical_as_int + ) + hoist$emit(glue("{tmp@name} = {code}")) + Fortran(tmp@name, tmp) +} + # Hoist `x` into a temporary variable unless it already renders as a bare -# variable name. Use this whenever the same operand is spliced into generated -# code more than once: Fortran evaluates intrinsic actual arguments before the -# call, so repeating an expression duplicates its side effects (e.g. RNG -# state via runif()). +# variable name or a literal constant. Use this whenever the same operand is +# spliced into generated code more than once: Fortran evaluates intrinsic +# actual arguments before the call, so repeating an expression duplicates +# its side effects (e.g. RNG state via runif()) -- which names and literals +# don't have. hoist_unless_name <- function(x, hoist) { stopifnot(inherits(x, Fortran), inherits(x@value, Variable)) code <- trimws(as.character(x)) if (!is.null(x@value@name) && identical(code, x@value@name)) { return(x) } - tmp <- hoist$declare_tmp( + if (grepl("^-?[0-9]+(\\.[0-9]+)?(_c_(int|double))?$", code)) { + return(x) + } + materialize_via_hoist( + x, mode = x@value@mode, dims = x@value@dims, + hoist = hoist, logical_as_int = logical_as_int(x@value) ) - hoist$emit(glue("{tmp@name} = {x}")) - Fortran(tmp@name, tmp) +} + +# Name of the call one frame above the current handler ("" at top level). +# Materialization decisions branch on it: a fill constructor or +# matrix(scalar, ...) may stay a scalar only where the parent broadcasts, +# spreads, or pads it. +# Used by: r2f-constructors.R +parent_call_name <- function(calls) { + if (length(calls) >= 2L) calls[[length(calls) - 1L]] else "" } @@ -155,13 +195,7 @@ lang2fortran <- r2f <- function( language = { # a call callable <- e[[1L]] - callable_unwrapped <- callable - while ( - is_call(callable_unwrapped, quote(`(`)) && - length(callable_unwrapped) == 2L - ) { - callable_unwrapped <- callable_unwrapped[[2L]] - } + callable_unwrapped <- unwrap_parens(callable) if (!is.null(scope)) { maybe_lower_local_closure_call( @@ -174,11 +208,7 @@ lang2fortran <- r2f <- function( { handler <- get_r2f_handler(callable_unwrapped) - match.fun <- if (inherits(handler, R2FHandler)) { - handler@match_fun - } else { - attr(handler, "match.fun", TRUE) - } + match.fun <- handler_field(handler, "match_fun", "match.fun") if (is.null(match.fun)) { match.fun <- get0( callable_unwrapped, @@ -286,15 +316,9 @@ lang2fortran <- r2f <- function( } }, - ## handling 'object' and 'closure' here are both bad ideas, - ## TODO: delete both - # "object" = { - # if (inherits(e, Variable)) - # e <- Fortran(character(), e) - # stopifnot(inherits(e, Fortran)) - # e - # }, - + # Top-level entry only: quick() hands the user's closure to r2f() to + # start translation (new_fortran_subroutine()); expressions inside + # compiled code never produce a closure here. closure = { if (is.null(name <- attr(e, "name", TRUE))) { name <- if (is.symbol(name <- substitute(e))) { @@ -380,54 +404,72 @@ num2fortran <- function(x) { get_r2f_handler <- function(name) { stopifnot("All functions called must be named as symbols" = is.symbol(name)) - get0(name, r2f_handlers) %||% + handler <- get0(name, r2f_handlers) %||% stop("Unsupported function: ", name, call. = FALSE) + resolve_handler_fun(handler) } -# --- Destination Helpers --- - -dest_supported_for_call <- function(call) { - if (!is.call(call)) { - return(FALSE) +# Swap in the handler's current namespace binding, so an instrumented or +# otherwise rebound copy is dispatched instead of the one captured at +# registration. Only handlers registered as namespace-level named functions +# carry a `fun_name`; for every other handler this is a property read and a +# return. See register_r2f_handler() for why the name is recorded. +resolve_handler_fun <- function(handler) { + # Only R2FHandler objects can carry a `fun_name`, so this doubles as the + # check that `handler` is one -- bare-function handlers read NULL here. + name <- handler_field(handler, "fun_name") + if (!is_string(name)) { + return(handler) } - unwrapped <- call - while (is_call(unwrapped, "(") && length(unwrapped) == 2L) { - unwrapped <- unwrapped[[2L]] + current <- get0(name, envir = environment(handler), mode = "function") + if (is.null(current) || identical(current, S7_data(handler))) { + return(handler) } - if (!is.call(unwrapped) || !is.symbol(unwrapped[[1L]])) { - return(FALSE) - } - handler <- get0(as.character(unwrapped[[1L]]), r2f_handlers, inherits = FALSE) + S7_data(handler) <- current + handler +} + + +# --- Destination Helpers --- + +# Read a handler property, whether the handler is an R2FHandler object or +# a bare function carrying attributes. `attr_name` covers the one legacy +# spelling difference (the "match.fun" attr vs the match_fun property). +# NULL handlers read as NULL. +handler_field <- function(handler, name, attr_name = name) { if (inherits(handler, R2FHandler)) { - isTRUE(handler@dest_supported) + prop(handler, name) } else { - isTRUE(attr(handler, "dest_supported", exact = TRUE)) + attr(handler, attr_name, exact = TRUE) } } -dest_infer_for_call <- function(call, scope) { +# Resolve the registered handler for a (possibly parenthesized) call, or +# NULL when it is not a named-symbol call or has no handler. +handler_for_call <- function(call) { if (!is.call(call)) { return(NULL) } - unwrapped <- call - while (is_call(unwrapped, "(") && length(unwrapped) == 2L) { - unwrapped <- unwrapped[[2L]] - } - if (!is.call(unwrapped) || !is.symbol(unwrapped[[1L]])) { + call <- unwrap_parens(call) + if (!is.call(call) || !is.symbol(call[[1L]])) { return(NULL) } - handler <- get0(as.character(unwrapped[[1L]]), r2f_handlers, inherits = FALSE) - infer <- if (inherits(handler, R2FHandler)) { - handler@dest_infer - } else { - attr(handler, "dest_infer", exact = TRUE) - } - infer_name <- if (inherits(handler, R2FHandler)) { - handler@dest_infer_name - } else { - attr(handler, "dest_infer_name", exact = TRUE) + get0(as.character(call[[1L]]), r2f_handlers, inherits = FALSE) +} + +dest_supported_for_call <- function(call) { + isTRUE(handler_field(handler_for_call(call), "dest_supported")) +} + +dest_infer_for_call <- function(call, scope) { + handler <- handler_for_call(call) + if (is.null(handler)) { + return(NULL) } + unwrapped <- unwrap_parens(call) + infer <- handler_field(handler, "dest_infer") + infer_name <- handler_field(handler, "dest_infer_name") infer_fun <- NULL if (is_string(infer_name)) { @@ -451,14 +493,6 @@ dest_infer_for_call <- function(call, scope) { # --- Default Handlers --- -r2f_default_handler <- function(args, scope = NULL, ..., calls) { - # stopifnot(is.call(e), is.symbol(e[[1L]])) - - x <- lapply(args, r2f, scope = scope, calls = calls, ...) - s <- sprintf("%s(%s)", last(calls), str_flatten_commas(x[-1])) - Fortran(s) -} - .r2f_handler_not_implemented_yet <- function(e, scope, ...) { stop( gettextf("'%s' is not implemented yet", as.character(e[[1L]])), diff --git a/R/r2f-arithmetic.R b/R/r2f-arithmetic.R index 4b13622..bcf176b 100644 --- a/R/r2f-arithmetic.R +++ b/R/r2f-arithmetic.R @@ -3,53 +3,75 @@ # --- Handlers --- -r2f_handlers[["+"]] <- function(args, scope, ...) { +r2f_handlers[["+"]] <- function(args, scope, ..., hoist = NULL) { # Support both binary and unary plus if (length(args) == 1L) { - x <- r2f(args[[1L]], scope, ...) + x <- r2f(args[[1L]], scope, ..., hoist = hoist) # R: +TRUE is 1L x <- cast_to_mode(x, arith_join_mode(x), "unary +") Fortran(glue("(+{x})"), Variable(x@value@mode, x@value@dims)) } else { - .[left, right] <- lapply(args, r2f, scope, ...) + .[left, right] <- lower_elementwise_operands( + args, + scope, + ..., + hoist = hoist + ) .[left, right] <- promote_arith_pair(left, right, "+") - .[left, right] <- maybe_reshape_vector_matrix(left, right) - Fortran(glue("({left} + {right})"), conform(left@value, right@value)) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + Fortran( + glue("({left} + {right})"), + infer_result_variable(left@value, right@value) + ) } } -r2f_handlers[["-"]] <- function(args, scope, ...) { +r2f_handlers[["-"]] <- function(args, scope, ..., hoist = NULL) { # Support both binary and unary minus if (length(args) == 1L) { - x <- r2f(args[[1L]], scope, ...) + x <- r2f(args[[1L]], scope, ..., hoist = hoist) # R: -TRUE is -1L x <- cast_to_mode(x, arith_join_mode(x), "unary -") Fortran(glue("(-{x})"), Variable(x@value@mode, x@value@dims)) } else { - .[left, right] <- lapply(args, r2f, scope, ...) + .[left, right] <- lower_elementwise_operands( + args, + scope, + ..., + hoist = hoist + ) .[left, right] <- promote_arith_pair(left, right, "-") - .[left, right] <- maybe_reshape_vector_matrix(left, right) - Fortran(glue("({left} - {right})"), conform(left@value, right@value)) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + Fortran( + glue("({left} - {right})"), + infer_result_variable(left@value, right@value) + ) } } -r2f_handlers[["*"]] <- function(args, scope = NULL, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) +r2f_handlers[["*"]] <- function(args, scope = NULL, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) .[left, right] <- promote_arith_pair(left, right, "*") - .[left, right] <- maybe_reshape_vector_matrix(left, right) - Fortran(glue("({left} * {right})"), conform(left@value, right@value)) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + Fortran( + glue("({left} * {right})"), + infer_result_variable(left@value, right@value) + ) } -r2f_handlers[["/"]] <- function(args, scope = NULL, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) +r2f_handlers[["/"]] <- function(args, scope = NULL, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) left <- maybe_cast_double(left) right <- maybe_cast_double(right) - .[left, right] <- maybe_reshape_vector_matrix(left, right) - Fortran(glue("({left} / {right})"), conform(left@value, right@value)) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + Fortran( + glue("({left} / {right})"), + infer_result_variable(left@value, right@value) + ) } -r2f_handlers[["^"]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) +r2f_handlers[["^"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) # R's ^ always returns double (R_pow), so cast the base. Keep an integer # exponent as integer: Fortran `real ** int` is exact and, unlike # `real ** real`, defined for negative bases -- matching R, which @@ -58,7 +80,7 @@ r2f_handlers[["^"]] <- function(args, scope, ...) { if (identical(right@value@mode, "logical")) { right <- cast_to_mode(right, "integer", "^") } - .[left, right] <- maybe_reshape_vector_matrix(left, right) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) mode <- reduce_promoted_mode(left, right) if (!identical(mode, "complex")) { mode <- "double" @@ -66,7 +88,7 @@ r2f_handlers[["^"]] <- function(args, scope, ...) { # Parenthesizing the exponent avoids non-standard `** -1_c_int`. Fortran( glue("({left} ** ({right}))"), - conform(left@value, right@value, mode = mode) + infer_result_variable(left@value, right@value, mode = mode) ) } @@ -83,22 +105,28 @@ r2f_handlers[["^"]] <- function(args, scope, ...) { # - FLOOR(x) : greatest integer <= x (real) # - AINT(x) : truncation toward 0 (real) -r2f_handlers[["%%"]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) +r2f_handlers[["%%"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) # `modulo` requires same-typed arguments, so cast both operands to the # join (logical joins as integer: R's TRUE %% TRUE is 0L). mode <- arith_join_mode(left, right) + if (identical(mode, "complex")) { + # Fortran modulo() has no complex form; R refuses too. + stop("unimplemented complex operation", call. = FALSE) + } left <- cast_to_mode(left, mode, "%%") right <- cast_to_mode(right, mode, "%%") - out_val <- conform(left@value, right@value) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + out_val <- infer_result_variable(left@value, right@value) # MODULO gives result with sign(right) - matches R %% behaviour Fortran(glue("modulo({left}, {right})"), out_val) } r2f_handlers[["%/%"]] <- function(args, scope, ..., hoist = NULL) { - .[left, right] <- lapply(args, r2f, scope, ..., hoist = hoist) + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) .[left, right] <- promote_arith_pair(left, right, "%/%") - out_val <- conform(left@value, right@value) + .[left, right] <- conform_elementwise_operands(left, right, hoist, scope) + out_val <- infer_result_variable(left@value, right@value) expr <- switch( out_val@mode, @@ -106,16 +134,13 @@ r2f_handlers[["%/%"]] <- function(args, scope, ..., hoist = NULL) { "int(floor(real({left}, kind=c_double) / real({right}, kind=c_double)), kind=c_int)" ), double = { - # Fortran FLOOR() returns an integer, so a large double quotient - # (e.g. 1e20 %/% 3) would silently overflow. Stay in the real domain - # as the floor() handler does; the quotient is spliced three times, - # so hoist it to evaluate once. + # The quotient is spliced three times by real_floor_expr(), so + # hoist it to evaluate once. q <- hoist_unless_name( Fortran(glue("({left} / {right})"), out_val), hoist ) - aint <- glue("aint({q})") - glue("({aint} - merge(1.0_c_double, 0.0_c_double, ({q} < {aint})))") + real_floor_expr(q) }, stop("%/% only implemented for numeric types") ) diff --git a/R/r2f-assign.R b/R/r2f-assign.R index 8cca99c..200a581 100644 --- a/R/r2f-assign.R +++ b/R/r2f-assign.R @@ -71,9 +71,6 @@ register_r2f_handler( return(out) } - # It sure seems like it's be nice if the Fortran() constructor - # took mode and dims as args directly, - # without needing to go through Variable... stopifnot(is.symbol(target)) name <- as.character(target) @@ -223,7 +220,13 @@ register_r2f_handler( var@dims <- value@value@dims } check_reassignment_narrowing(name, var, value@value) - check_assignment_compatible(var, value@value) + check_assignment_compatible( + name, + var, + value@value, + hoist = hoist, + scope = scope + ) var@modified <- TRUE # could probably drop this @modified property, and instead track # if the var populated by declare is identical at the end (e.g., perhaps by @@ -270,6 +273,37 @@ register_r2f_handler( } ) +# Validate and resolve the target of a superassignment (`x <<- v`, +# `x[i] <<- v`) to its host-scope Variable: the name must not shadow a +# closure formal or the closure's output variable, and must already exist +# in the enclosing quick() scope. Marks the host variable modified. +# Used by: `<<-`, `[<<-`, compile_subscript_lhs() (r2f-closures.R) +resolve_superassign_target <- function(name, scope) { + formal_names <- names(formals(scope_closure(scope))) %||% character() + if (name %in% formal_names) { + stop("<<- targets must not shadow closure formals: ", name) + } + + forbidden <- scope_forbid_superassign(scope) + if (name %in% forbidden) { + stop("closure must not superassign to its output variable: ", name) + } + + host_scope <- scope_host_scope(scope) %||% + stop("internal error: missing host scope") + host_var <- get0(name, host_scope) + if (!inherits(host_var, Variable)) { + stop( + "<<- targets must resolve to an existing variable in the enclosing quick() scope: ", + name + ) + } + + host_var@modified <- TRUE + host_scope[[name]] <- host_var + host_var +} + register_r2f_handler( "<<-", function(args, scope, ..., hoist = NULL) { @@ -296,32 +330,17 @@ register_r2f_handler( stopifnot(is.symbol(target)) name <- as.character(target) - formal_names <- names(formals(scope_closure(scope))) %||% character() - if (name %in% formal_names) { - stop("<<- targets must not shadow closure formals: ", name) - } - - forbidden <- scope_forbid_superassign(scope) - if (name %in% forbidden) { - stop("closure must not superassign to its output variable: ", name) - } - - host_scope <- scope_host_scope(scope) %||% - stop("internal error: missing host scope") - host_var <- get0(name, host_scope) - if (!inherits(host_var, Variable)) { - stop( - "<<- targets must resolve to an existing variable in the enclosing quick() scope: ", - name - ) - } - - host_var@modified <- TRUE - host_scope[[name]] <- host_var + host_var <- resolve_superassign_target(name, scope) value <- r2f(args[[2L]], scope, ..., hoist = hoist) check_reassignment_narrowing(name, host_var, value@value) - check_assignment_compatible(host_var, value@value) + check_assignment_compatible( + name, + host_var, + value@value, + hoist = hoist, + scope = scope + ) Fortran(glue("{host_var@name} = {value}")) } @@ -343,28 +362,7 @@ register_r2f_handler( } name <- as.character(base) - formal_names <- names(formals(scope_closure(scope))) %||% character() - if (name %in% formal_names) { - stop("<<- targets must not shadow closure formals: ", name) - } - - forbidden <- scope_forbid_superassign(scope) - if (name %in% forbidden) { - stop("closure must not superassign to its output variable: ", name) - } - - host_scope <- scope_host_scope(scope) %||% - stop("internal error: missing host scope") - host_var <- get0(name, host_scope) - if (!inherits(host_var, Variable)) { - stop( - "<<- targets must resolve to an existing variable in the enclosing quick() scope: ", - name - ) - } - - host_var@modified <- TRUE - host_scope[[name]] <- host_var + host_var <- resolve_superassign_target(name, scope) lhs <- compile_subscript_lhs( subset_call, diff --git a/R/r2f-closures.R b/R/r2f-closures.R index f2f31e9..31767d4 100644 --- a/R/r2f-closures.R +++ b/R/r2f-closures.R @@ -396,7 +396,7 @@ compile_internal_subroutine <- function( body_code <- str_flatten_lines(optional_inits, body_prefix, assign_code) used_iso_bindings <- iso_c_binding_symbols( vars = vars_declared, - body_code = body_code, + body_code = str_flatten_lines(decls, body_code), logical_is_c_int = logical_as_int, uses_rng = FALSE ) @@ -1394,38 +1394,16 @@ compile_subset_designator <- function( # silent out-of-bounds Fortran writes. check_subscript_exprs(base_var, idx_args) - idxs <- whole_doubles_to_ints(idx_args) - idxs <- imap(idxs, function(idx, i) { - if (is_missing(idx)) { - Fortran(":", Variable("integer", base_var@dims[[i]])) - } else { - sub <- r2f(idx, scope, ..., hoist = hoist) - if (sub@value@mode == "double") { - Fortran( - glue("int({sub}, kind=c_ptrdiff_t)"), - Variable("integer", sub@value@dims) - ) - } else { - sub - } - } - }) + idxs <- lower_subscript_args( + idx_args, + base_var@dims, + scope, + ..., + hoist = hoist + ) - # Indexing a scalar (rank-1 length-1) with `[1]` is valid in R, but Fortran - # scalars cannot be subscripted. Treat it as a no-op. - if ( - passes_as_scalar(base_var) && - length(idxs) == 1 && - idxs[[1]]@value@mode == "integer" && - passes_as_scalar(idxs[[1]]@value) - ) { - idx_r <- attr(idxs[[1]], "r", exact = TRUE) - if (identical(idx_r, 1L) || identical(idx_r, 1)) { - return(base_name) - } - if (isTRUE(idxs[[1]]@value@loop_is_singleton)) { - return(base_name) - } + if (subscript_is_scalar_noop(base_var, idxs)) { + return(base_name) } # R-style linear indexing for rank>1 arrays: x[i] @@ -1462,13 +1440,13 @@ compile_subset_designator <- function( mask <- booleanize_logical_as_int(subscript) it <- scope_unique_var(scope, "integer") f <- glue("pack([({it}, {it}=1, size({mask}))], {mask})") - Fortran(f, Variable("int", NA)) + Fortran(f, Variable("integer", NA)) }, integer0 = { if (drop) { subscript } else { - Fortran(glue("{subscript}:{subscript}"), Variable("int", 1)) + Fortran(glue("{subscript}:{subscript}"), Variable("integer", 1)) } }, integer1 = { @@ -1570,15 +1548,7 @@ compile_subscript_lhs <- function( } name <- as.character(base) - host_scope <- scope_host_scope(scope) %||% - stop("internal error: missing host scope") - host_var <- get0(name, host_scope) - if (!inherits(host_var, Variable)) { - stop( - "<<- targets must resolve to an existing variable in the enclosing quick() scope: ", - name - ) - } + host_var <- resolve_superassign_target(name, scope) idx_args <- as.list(subset_call)[-1L] idx_args <- idx_args[-1L] diff --git a/R/r2f-coercions.R b/R/r2f-coercions.R index e4362d5..40ecf92 100644 --- a/R/r2f-coercions.R +++ b/R/r2f-coercions.R @@ -1,5 +1,5 @@ # r2f-coercions.R -# Handlers for type coercions: as.double, as.integer +# Handlers for type coercions: as.double, as.integer, as.vector # --- Handlers --- @@ -9,24 +9,7 @@ r2f_handlers[["as.double"]] <- function(args, scope = NULL, ...) { x <- maybe_cast_double(x) # R drops dimensions for as.double(): the result is a vector. - if (!passes_as_scalar(x@value) && x@value@rank > 1L) { - len_expr <- value_length_expr(x@value) - len_str <- if (is_scalar_na(len_expr)) { - glue("size({x})") - } else { - # dims2f() returns "" for a scalar "1", but we need a literal length. - out <- dims2f(list(len_expr), scope) - if (!nzchar(out)) "1" else out - } - - out_val <- Variable( - "double", - list(if (is_scalar_na(len_expr)) NA else len_expr) - ) - return(Fortran(glue("reshape({x}, [{len_str}])"), out_val)) - } - - x + flatten_to_vector(x, scope) } r2f_handlers[["as.integer"]] <- function(args, scope = NULL, ...) { @@ -45,32 +28,62 @@ r2f_handlers[["as.integer"]] <- function(args, scope = NULL, ...) { double = Fortran(glue("int({arg}, kind=c_int)"), out_val), logical = { # External logicals are integer-backed (0/1/NA) under bind(c); if the - # expression preserves that storage (e.g. rev(m)), return it directly. + # expression preserves that storage (e.g. rev(m)), reuse it directly. if (logical_as_int(arg@value)) { - src <- arg@value@name %||% as.character(arg) - return(Fortran(src, out_val)) + Fortran(arg@value@name %||% as.character(arg), out_val) + } else { + arg <- booleanize_logical_as_int(arg) + Fortran(glue("merge(1_c_int, 0_c_int, {arg})"), out_val) } - arg <- booleanize_logical_as_int(arg) - Fortran(glue("merge(1_c_int, 0_c_int, {arg})"), out_val) }, stop("as.integer() only implemented for logical, integer, and double") ) # R drops dimensions for as.integer(): the result is a vector. - if (!passes_as_scalar(out@value) && out@value@rank > 1L) { - len_expr <- value_length_expr(out@value) - len_str <- if (is_scalar_na(len_expr)) { - glue("size({out})") - } else { - out_len <- dims2f(list(len_expr), scope) - if (!nzchar(out_len)) "1" else out_len - } - out_val <- Variable( - "integer", - list(if (is_scalar_na(len_expr)) NA else len_expr) + flatten_to_vector(out, scope) +} + +r2f_handlers[["as.vector"]] <- function(args, scope = NULL, ...) { + x_arg <- args$x %||% if (length(args) >= 1L) args[[1L]] else NULL + if (is.null(x_arg) || is_missing(x_arg)) { + stop("as.vector() expects `x`", call. = FALSE) + } + + # `mode` selects the coercion; "any" (the default) preserves the type + # and only drops dimensions. Numeric modes delegate to the dedicated + # coercion handlers so the cast spellings stay in one place. + mode_arg <- args$mode %||% if (length(args) >= 2L) args[[2L]] else NULL + mode <- if (is.null(mode_arg) || is_missing(mode_arg)) { + "any" + } else if (is.character(mode_arg) && length(mode_arg) == 1L) { + mode_arg + } else { + stop("as.vector() `mode` must be a string constant", call. = FALSE) + } + + if (mode %in% c("double", "numeric")) { + return(r2f(as.call(list(quote(as.double), x_arg)), scope, ...)) + } + if (mode == "integer") { + return(r2f(as.call(list(quote(as.integer), x_arg)), scope, ...)) + } + if (!mode %in% c("any", "logical", "complex")) { + stop( + "as.vector() does not support mode = ", + encodeString(mode, quote = "\""), + call. = FALSE ) - return(Fortran(glue("reshape({out}, [{len_str}])"), out_val)) } - out + x <- r2f(x_arg, scope, ...) + if (mode != "any" && !identical(x@value@mode, mode)) { + stop( + "as.vector(x, mode = ", + encodeString(mode, quote = "\""), + ") requires an operand already of that mode; casting is only ", + "supported for numeric modes", + call. = FALSE + ) + } + flatten_to_vector(x, scope) } diff --git a/R/r2f-conditionals.R b/R/r2f-conditionals.R index 9e76761..746d454 100644 --- a/R/r2f-conditionals.R +++ b/R/r2f-conditionals.R @@ -8,34 +8,14 @@ ifelse_branch_shape_msg <- paste0( "R-style recycling is not supported" ) -# Three-valued conformability verdict for one axis of an ifelse() branch -# against `test`: ok+known (no guard), not-ok+known (compile error), or -# unknown (runtime guard). NA dims are always unknown: two unknown lengths -# are not the same quantity. -ifelse_axis_verdict <- function(test_dim, branch_dim) { - if (is_wholenumber(test_dim) && is_wholenumber(branch_dim)) { - return(list( - ok = identical(as.integer(test_dim), as.integer(branch_dim)), - unknown = FALSE - )) - } - if (!is_scalar_na(test_dim) && !is_scalar_na(branch_dim)) { - test_norm <- fortranize_expr_symbols(test_dim) - branch_norm <- fortranize_expr_symbols(branch_dim) - if (identical(test_norm, branch_norm)) { - return(list(ok = TRUE, unknown = FALSE)) - } - } - list(ok = TRUE, unknown = TRUE) -} - # Enforce the shape contract for one ifelse() branch: scalars broadcast # natively; a non-scalar branch must match `test`'s shape, because # merge() requires conformable arguments and a runtime mismatch would -# read past the shorter branch. Statically unequal dims are a compile -# error; symbolic dims get a statement-level runtime size guard, emitted into -# `hoist` -- always a live hoist context, since r2f() substitutes a fresh one -# before dispatching to any handler. +# read past the shorter branch. Per axis, guard_conformable_dims() +# applies the framework policy: statically unequal dims are a compile +# error; symbolic dims get a statement-level runtime size guard, emitted +# into `hoist` -- always a live hoist context, since r2f() substitutes a +# fresh one before dispatching to any handler. check_ifelse_branch_shape <- function(branch, mask, hoist, scope) { if (passes_as_scalar(branch@value)) { return(invisible()) @@ -43,32 +23,19 @@ check_ifelse_branch_shape <- function(branch, mask, hoist, scope) { if (branch@value@rank != mask@value@rank) { stop(ifelse_branch_shape_msg, call. = FALSE) } - unknown_axes <- integer() for (axis in seq_len(mask@value@rank)) { - verdict <- ifelse_axis_verdict( + guard_conformable_dims( + dim_or_one(branch, axis), dim_or_one(mask, axis), - dim_or_one(branch, axis) + ifelse_branch_shape_msg, + hoist, + scope, + left = branch, + right = mask, + left_axis = axis, + right_axis = axis ) - if (!verdict$ok) { - stop(ifelse_branch_shape_msg, call. = FALSE) - } - if (verdict$unknown) { - unknown_axes <- c(unknown_axes, axis) - } } - if (!length(unknown_axes)) { - return(invisible()) - } - # size() is an inquiry, so applying it to operand expression text does - # not evaluate the operands. - condition <- str_flatten( - map_chr( - unknown_axes, - function(axis) glue("size({branch}, {axis}) /= size({mask}, {axis})") - ), - " .or. " - ) - emit_quickr_error_if(condition, ifelse_branch_shape_msg, hoist, scope) invisible() } diff --git a/R/r2f-constructors.R b/R/r2f-constructors.R index a8e085e..d041a48 100644 --- a/R/r2f-constructors.R +++ b/R/r2f-constructors.R @@ -2,16 +2,170 @@ # Handlers for value constructors: c, logical, integer, double, numeric, # character, raw, matrix, array +# --- Helpers --- + +# TRUE for calls to the zero-fill constructors: logical(k), integer(k), +# double(k), numeric(k). These lower to a single scalar literal carrying +# array dims, so splicing contexts must spread them explicitly. +# Used by: c(), array() +is_fill_constructor_call <- function(e, scope) { + if (!is.call(e) || !is.symbol(e[[1L]])) { + return(FALSE) + } + name <- as.character(e[[1L]]) + name %in% + c("logical", "integer", "double", "numeric") && + (is.null(scope) || !inherits(scope[[name]], LocalClosure)) +} + +# (parent_call_name() and materialize_via_hoist(), which the handlers +# below build on, live with the hoisting infrastructure in +# r2f-aab-core.R.) + +# Parse array(dim=)'s argument to a dims list: literal vectors, literal +# `a:b` sequences, and symbols bound to a known literal vector; anything +# else falls through to r2dims(). +# Used by: array() +parse_array_dims <- function(dim_arg, scope) { + if ( + is.atomic(dim_arg) && + typeof(dim_arg) %in% c("integer", "double") + ) { + if (!length(dim_arg) || anyNA(dim_arg)) { + stop( + "array(dim=) must be non-empty and must not contain NA", + call. = FALSE + ) + } + dim_arg <- vapply( + dim_arg, + function(x) { + if (!is_wholenumber(x)) { + stop( + "array(dim=) must be whole numbers, found: ", + x, + call. = FALSE + ) + } + as.integer(x) + }, + integer(1L) + ) + return(as.list(dim_arg)) + } + + if (is.call(dim_arg) && is.symbol(dim_arg[[1L]])) { + op <- as.character(dim_arg[[1L]]) + if (op == ":") { + if (length(dim_arg) != 3L) { + stop("bad dim sequence", call. = FALSE) + } + from <- dim_arg[[2L]] + to <- dim_arg[[3L]] + if ( + !(is.atomic(from) && length(from) == 1L && is_wholenumber(from)) || + !(is.atomic(to) && length(to) == 1L && is_wholenumber(to)) + ) { + stop( + "array(dim=) only supports literal sequences like 2:4", + call. = FALSE + ) + } + return(as.list(seq.int(as.integer(from), as.integer(to)))) + } + } + + if (is.symbol(dim_arg)) { + var <- get0(as.character(dim_arg), scope) + if ( + inherits(var, Variable) && + var@mode %in% c("integer", "double") && + var@rank == 1L && + (is.language(var@r) || is.atomic(var@r)) && + !identical(var@r, dim_arg) + ) { + return(parse_array_dims(var@r, scope)) + } + } + + r2dims(dim_arg, scope) +} + +# Product of a dims list when every dim is a known whole number, NA_real_ +# otherwise (in double to dodge integer overflow on large dims). +# Used by: array() +known_dims_product <- function(dims) { + if (is.null(dims) || !length(dims)) { + return(1) + } + vals <- vapply( + dims, + function(d) { + if ( + is.atomic(d) && + length(d) == 1L && + !is.na(d) && + is_wholenumber(d) + ) { + as.double(d) + } else { + NA_real_ + } + }, + double(1L) + ) + if (anyNA(vals)) { + return(NA_real_) + } + prod(vals) +} + # --- Handlers --- r2f_handlers[["c"]] <- function(args, scope = NULL, ...) { ff <- lapply(args, r2f, scope, ...) + # R's c() flattens matrix/array arguments column-major; drop their dims + # to a rank-1 view before joining (fill constructors are already + # rank-1, so they pass through untouched for the spread below). + ff <- lapply(ff, flatten_to_vector, scope = scope) # Fortran array constructors require uniform element types; cast every # element whose mode differs from the promoted mode (R: c(1L, 2.5) is # double, c(TRUE, 2L) is integer). promoted <- promote_operands(ff, context = "c()") ff <- promoted$args mode <- promoted$mode + # Fill constructors are one scalar literal claiming length k; spread them + # as implied-dos so the emitted element count matches the claimed length. + fill_idx <- which(map_lgl(args, is_fill_constructor_call, scope = scope)) + if (length(fill_idx)) { + spread_var <- NULL + for (j in fill_idx) { + len_f <- dims2f(ff[[j]]@value@dims, scope) + if (!nzchar(len_f)) { + next # statically length 1: a single spliced scalar is already right + } + if (grepl(":", len_f, fixed = TRUE)) { + stop( + "the length of ", + deparse1(args[[j]]), + " inside c() must be known", + call. = FALSE + ) + } + spread_var <- spread_var %||% + scope_unique_var( + scope, + "integer", + integer_kind = "c_ptrdiff_t" + ) + ff[[j]] <- Fortran( + glue( + "({ff[[j]]}, {spread_var}=1_c_ptrdiff_t, int({len_f}, kind=c_ptrdiff_t))" + ), + ff[[j]]@value + ) + } + } s <- glue("[ {str_flatten_commas(ff)} ]") lens <- lapply(ff[order(map_int(ff, \(f) f@value@rank))], function(e) { rank <- e@value@rank @@ -106,26 +260,68 @@ r2f_handlers[["rep.int"]] <- function(args, scope, ..., hoist = NULL) { } +# Compile a zero-fill constructor call: a single scalar literal carrying +# array dims. Whole-array assignment broadcasts that correctly, and +# c()/array() spread it as an implied-do, so those contexts keep the +# scalar form. Any other consumer (elementwise ops, reductions, +# matrix() -- whose reshape() lowering needs an array SOURCE, not a +# scalar literal) needs a real array expression -- an expression like +# `numeric(2) + 1` would otherwise contribute one element where its dims +# claim two -- so materialize the fill into a hoisted temporary there. +fill_constructor_value <- function(literal, mode, args, scope, ..., hoist) { + var <- Variable(mode = mode, dims = r2dims(args, scope)) + out <- Fortran(literal, var) + if (passes_as_scalar(var)) { + return(out) + } + parent_call <- parent_call_name(list(...)$calls) + if (parent_call %in% c("<-", "=", "<<-", "c", "array")) { + return(out) + } + materialize_via_hoist(literal, mode, var@dims, hoist) +} + register_r2f_handler( "logical", - function(args, scope, ...) { - Fortran(".false.", Variable(mode = "logical", dims = r2dims(args, scope))) + function(args, scope, ..., hoist = NULL) { + fill_constructor_value( + ".false.", + "logical", + args, + scope, + ..., + hoist = hoist + ) }, match_fun = FALSE ) register_r2f_handler( "integer", - function(args, scope, ...) { - Fortran("0", Variable(mode = "integer", dims = r2dims(args, scope))) + function(args, scope, ..., hoist = NULL) { + fill_constructor_value( + "0_c_int", + "integer", + args, + scope, + ..., + hoist = hoist + ) }, match_fun = FALSE ) register_r2f_handler( c("double", "numeric"), - function(args, scope, ...) { - Fortran("0", Variable(mode = "double", dims = r2dims(args, scope))) + function(args, scope, ..., hoist = NULL) { + fill_constructor_value( + "0.0_c_double", + "double", + args, + scope, + ..., + hoist = hoist + ) }, match_fun = FALSE ) @@ -135,42 +331,62 @@ r2f_handlers[["character"]] <- r2f_handlers[["raw"]] <- .r2f_handler_not_implemented_yet -r2f_handlers[["matrix"]] <- function(args, scope = NULL, ..., hoist = NULL) { - args$data %||% stop("matrix(data=) must be provided, cannot be NA") +# Validate matrix()'s matched arguments once for every consumer: the +# matrix() handler and the elementwise scalar-fill fast path +# (match_scalar_matrix_fill() in r2f-operators-helpers.R), so the two cannot +# drift as more vectorization contexts are added. The shared policy: +# data is required, byrow=TRUE and dimnames are unsupported, and +# nrow/ncol are both required. (R can infer one dimension, but quickr's +# lowering keeps this strict to avoid surprising recycling rules.) +# Returns list(data, nrow, ncol) or stops. +matrix_call_args <- function(args) { + data <- args$data + if (is.null(data) || is_missing(data)) { + stop("matrix(data=) must be provided, cannot be NA", call. = FALSE) + } if (!is.null(args$byrow) && !is_missing(args$byrow) && !isFALSE(args$byrow)) { stop("matrix(byrow=TRUE) is not supported", call. = FALSE) } - - # Require explicit dims for now. (R can infer one dimension, but quickr's - # lowering keeps this strict to avoid surprising recycling rules.) + if ( + !is.null(args$dimnames) && + !is_missing(args$dimnames) && + !identical(args$dimnames, quote(NULL)) + ) { + stop("matrix(dimnames=) not supported", call. = FALSE) + } if (is.null(args$nrow) || is_missing(args$nrow)) { stop("matrix(nrow=) must be provided", call. = FALSE) } if (is.null(args$ncol) || is_missing(args$ncol)) { stop("matrix(ncol=) must be provided", call. = FALSE) } + list(data = data, nrow = args$nrow, ncol = args$ncol) +} + +r2f_handlers[["matrix"]] <- function(args, scope = NULL, ..., hoist = NULL) { + margs <- matrix_call_args(args) - src <- r2f(args$data, scope, ..., hoist = hoist) - dims <- r2dims(list(args$nrow, args$ncol), scope) + src <- r2f(margs$data, scope, ..., hoist = hoist) + dims <- r2dims(list(margs$nrow, margs$ncol), scope) out_val <- Variable(mode = src@value@mode, dims = dims) - # Scalars can be broadcast into an array on assignment, so keep them as-is. + # A scalar broadcasts natively on direct whole-array assignment, so keep + # it as-is there; in any other context (sum(...), %*%, ...) the expression + # must be a real rank-2 array, so materialize it into a hoisted temporary. if (passes_as_scalar(src@value)) { - src@value <- out_val - return(src) + if (parent_call_name(list(...)$calls) %in% c("<-", "=", "<<-")) { + src@value <- out_val + return(src) + } + return(materialize_via_hoist(src, src@value@mode, dims, hoist)) } - rows <- dims[[1L]] - cols <- dims[[2L]] - - # Avoid double-evaluating non-trivial expressions when used in both the - # `source` and `pad` args. - source <- glue("{hoist_unless_name(src, hoist)}") - Fortran( - glue( - "reshape({source}, [{bind_dim_int(rows)}, {bind_dim_int(cols)}], pad = {source})" - ), - out_val + # reshape_vector_for_matrix() splices its source into both the `source` + # and `pad` args; hoist non-trivial expressions so they evaluate once. + reshape_vector_for_matrix( + hoist_unless_name(src, hoist), + dims[[1L]], + dims[[2L]] ) } @@ -183,73 +399,8 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { stop("array(dimnames=) not supported") } - dim_to_dims <- function(dim_arg) { - if ( - is.atomic(dim_arg) && - typeof(dim_arg) %in% c("integer", "double") - ) { - if (!length(dim_arg) || anyNA(dim_arg)) { - stop( - "array(dim=) must be non-empty and must not contain NA", - call. = FALSE - ) - } - dim_arg <- vapply( - dim_arg, - function(x) { - if (!is_wholenumber(x)) { - stop( - "array(dim=) must be whole numbers, found: ", - x, - call. = FALSE - ) - } - as.integer(x) - }, - integer(1L) - ) - return(as.list(dim_arg)) - } - - if (is.call(dim_arg) && is.symbol(dim_arg[[1L]])) { - op <- as.character(dim_arg[[1L]]) - if (op == ":") { - if (length(dim_arg) != 3L) { - stop("bad dim sequence", call. = FALSE) - } - from <- dim_arg[[2L]] - to <- dim_arg[[3L]] - if ( - !(is.atomic(from) && length(from) == 1L && is_wholenumber(from)) || - !(is.atomic(to) && length(to) == 1L && is_wholenumber(to)) - ) { - stop( - "array(dim=) only supports literal sequences like 2:4", - call. = FALSE - ) - } - return(as.list(seq.int(as.integer(from), as.integer(to)))) - } - } - - if (is.symbol(dim_arg)) { - var <- get0(as.character(dim_arg), scope) - if ( - inherits(var, Variable) && - var@mode %in% c("integer", "double") && - var@rank == 1L && - (is.language(var@r) || is.atomic(var@r)) && - !identical(var@r, dim_arg) - ) { - return(dim_to_dims(var@r)) - } - } - - r2dims(dim_arg, scope) - } - out <- r2f(args$data, scope, ..., hoist = hoist) - target_dims <- dim_to_dims(args$dim) + target_dims <- parse_array_dims(args$dim, scope) if (!length(target_dims)) { stop("array(dim=) must not be empty", call. = FALSE) } @@ -262,15 +413,16 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { if (scalar_target) { # `dim = 1` is scalar-like in quickr (rank-1 length-1 is declared scalar). # Avoid `reshape(..., [1])` (rank-1) and instead return the first element. - if (is.null(hoist)) { - stop("internal error: array() requires hoist context", call. = FALSE) - } target_dims <- list(1L) - tmp <- hoist$declare_tmp(mode = out@value@mode, dims = out@value@dims) - hoist$emit(glue("{tmp@name} = {out}")) + tmp <- materialize_via_hoist( + out, + mode = out@value@mode, + dims = out@value@dims, + hoist = hoist + ) idxs <- rep("1", out@value@rank) out <- Fortran( - glue("{tmp@name}({str_flatten_commas(idxs)})"), + glue("{tmp}({str_flatten_commas(idxs)})"), Variable(mode = out@value@mode, dims = list(1L)) ) } else { @@ -282,17 +434,7 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { } shape <- glue("int([{dims_f}])") - data_r <- args$data - is_fill_constructor <- - is.call(data_r) && - is.symbol(data_r[[1L]]) && - as.character(data_r[[1L]]) %in% - c( - "logical", - "integer", - "double", - "numeric" - ) + is_fill_constructor <- is_fill_constructor_call(args$data, scope) axis_terms <- vapply( target_dims, @@ -312,38 +454,12 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { paste0("(", paste0("(", axis_terms, ")", collapse = " * "), ")") } - known_prod <- function(dims) { - if (is.null(dims) || !length(dims)) { - return(1) - } - vals <- vapply( - dims, - function(d) { - if ( - is.atomic(d) && - length(d) == 1L && - !is.na(d) && - is_wholenumber(d) - ) { - as.double(d) - } else { - NA_real_ - } - }, - double(1L) - ) - if (anyNA(vals)) { - return(NA_real_) - } - prod(vals) - } - source <- if (is_fill_constructor) { i <- scope_unique_var(scope, "integer") glue("[({out}, {i}=1, int({n_expr}))]") } else { - n_target <- known_prod(target_dims) - n_source <- known_prod(out@value@dims) + n_target <- known_dims_product(target_dims) + n_source <- known_dims_product(out@value@dims) if (!is.na(n_target) && !is.na(n_source) && n_target > n_source) { stop( "array() reshape does not support recycling: prod(dim)=", @@ -354,14 +470,15 @@ r2f_handlers[["array"]] <- function(args, scope = NULL, ..., hoist = NULL) { ) } if (!is.null(hoist)) { - mark_scope_uses_errors(scope) - err <- quickr_error_fortran_lines( - "array() reshape does not support recycling (data shorter than prod(dim))", + emit_quickr_error_if( + condition = glue("int({n_expr}) > size({out})"), + message = paste0( + "array() reshape does not support recycling ", + "(data shorter than prod(dim))" + ), + hoist = hoist, scope = scope ) - hoist$emit(glue("if (int({n_expr}) > size({out})) then")) - hoist$emit(paste0(" ", err)) - hoist$emit("end if") } # RESHAPE() requires `SOURCE` to be an array expression; array constructors diff --git a/R/r2f-control-flow.R b/R/r2f-control-flow.R index 68f51f3..2905763 100644 --- a/R/r2f-control-flow.R +++ b/R/r2f-control-flow.R @@ -38,9 +38,13 @@ r2f_handlers[["if"]] <- function(args, scope, ..., hoist = NULL) { # TODO: return # ---- repeat ---- -r2f_handlers[["repeat"]] <- function(args, scope, ...) { +r2f_handlers[["repeat"]] <- function(args, scope, ..., hoist = NULL) { stopifnot(length(args) == 1L) - body <- r2f(args[[1]], scope, ...) + # The body gets its own hoist target: forwarding the enclosing + # statement's hoist would emit a single-statement body's hoisted code + # (BLAS calls, temporaries, guards) once, before the loop, instead of + # per iteration. (`{` bodies already isolate each statement.) + body <- r2f(args[[1]], scope, ..., hoist = NULL) check_pending_parallel_consumed(scope) Fortran(glue( "do @@ -63,13 +67,34 @@ r2f_handlers[["next"]] <- function(args, scope, ...) { } # ---- while ---- -r2f_handlers[["while"]] <- function(args, scope, ...) { +r2f_handlers[["while"]] <- function(args, scope, ..., hoist = NULL) { stopifnot(length(args) == 2L) - cond <- r2f(args[[1]], scope, ...) - body <- r2f(args[[2]], scope, ...) ## should we set a new hoist target here? + # The condition is re-evaluated every iteration, so any statements its + # translation hoists (e.g. the conditional lowering of `&&`/`||`) must + # re-run inside the loop -- the enclosing statement's hoist would + # evaluate them once, before the loop. Collect them separately and, when + # present, lower to an explicit exit check at the top of the loop body. + cond_hoist <- new_hoist(scope) + cond <- r2f(args[[1]], scope, ..., hoist = cond_hoist) + # The body gets its own hoist target for the same reason: forwarding the + # enclosing statement's hoist would emit a single-statement body's + # hoisted code (BLAS calls, temporaries, guards) once, before the loop. + # (`{` bodies already isolate each statement.) + body <- r2f(args[[2]], scope, ..., hoist = NULL) check_pending_parallel_consumed(scope) + if (cond_hoist$is_empty()) { + # nothing hoisted: keep the plain do-while form + return(Fortran(glue( + "do while ({cond}) + {indent(body)} + end do + " + ))) + } + cond_code <- cond_hoist$render(glue("if (.not. ({cond})) exit")) Fortran(glue( - "do while ({cond}) + "do + {indent(cond_code)} {indent(body)} end do " @@ -77,6 +102,41 @@ r2f_handlers[["while"]] <- function(args, scope, ...) { } # ---- for ---- + +# Compile a `for` body in its own per-statement hoist (see the `while` +# handler: forwarding the enclosing statement's hoist would emit a +# single-statement body's hoisted code once, before the loop), entering +# an OpenMP scope around the compile when the loop is parallel. Returns +# the compiled body together with the loop's OpenMP directives and +# post-loop error check, which must be computed while the OpenMP scope is +# still entered. +compile_for_body <- function(body, scope, ..., parallel, private = NULL) { + if (!is.null(parallel)) { + previous_openmp <- enter_openmp_scope(scope) + on.exit(exit_openmp_scope(scope, previous_openmp), add = TRUE) + } + body <- r2f(body, scope, ..., hoist = NULL) + check_pending_parallel_consumed(scope) + + directives <- openmp_directives(parallel, private = private) + if (!is.null(parallel)) { + mark_openmp_used(scope) + } + error_check_after <- if (!is.null(parallel)) { + quickr_error_return_if_set( + scope, + openmp_depth = scope_openmp_depth(scope) - 1L + ) + } else { + "" + } + list( + body = body, + directives = directives, + error_check_after = error_check_after + ) +} + r2f_handlers[["for"]] <- function(args, scope, ..., hoist = NULL) { .[var, iterable, body] <- args stopifnot(is.symbol(var)) @@ -170,16 +230,17 @@ r2f_handlers[["for"]] <- function(args, scope, ..., hoist = NULL) { } } - if (!is.null(parallel)) { - previous_openmp <- enter_openmp_scope(scope) - on.exit(exit_openmp_scope(scope, previous_openmp), add = TRUE) - } - # The body is a distinct execution region and needs its own hoist target. - # Otherwise a single-expression body reuses the enclosing statement's - # target and emits loop-dependent setup before the loop. - body <- r2f(body, scope, ..., hoist = NULL) - check_pending_parallel_consumed(scope) - loop_stmts <- str_flatten_lines(glue("{var_name} = {element_expr}"), body) + compiled <- compile_for_body( + body, + scope, + ..., + parallel = parallel, + private = var_name + ) + loop_stmts <- str_flatten_lines( + glue("{var_name} = {element_expr}"), + compiled$body + ) loop_header <- if (iterable_reversed) { glue("do {idx@name} = {end}, 1_c_int, -1_c_int") @@ -187,25 +248,13 @@ r2f_handlers[["for"]] <- function(args, scope, ..., hoist = NULL) { glue("do {idx@name} = 1_c_int, {end}") } - directives <- openmp_directives(parallel, private = var_name) - if (!is.null(parallel)) { - mark_openmp_used(scope) - } - error_check_after <- if (!is.null(parallel)) { - quickr_error_return_if_set( - scope, - openmp_depth = scope_openmp_depth(scope) - 1L - ) - } else { - "" - } return(Fortran(glue( " {iterable_tmp_assign} - {str_flatten_lines(directives$prefix, loop_header)} + {str_flatten_lines(compiled$directives$prefix, loop_header)} {indent(loop_stmts)} end do - {str_flatten_lines(directives$suffix, error_check_after)} + {str_flatten_lines(compiled$directives$suffix, compiled$error_check_after)} " ))) } @@ -218,33 +267,14 @@ r2f_handlers[["for"]] <- function(args, scope, ..., hoist = NULL) { scope[[var]] <- loop_var iterable <- r2f_for_iterable(iterable, scope, ..., hoist = hoist) - if (!is.null(parallel)) { - previous_openmp <- enter_openmp_scope(scope) - on.exit(exit_openmp_scope(scope, previous_openmp), add = TRUE) - } - # See the value-iteration path above: body-local setup must run inside the - # loop even when the R body is not wrapped in braces. - body <- r2f(body, scope, ..., hoist = NULL) - check_pending_parallel_consumed(scope) + compiled <- compile_for_body(body, scope, ..., parallel = parallel) - directives <- openmp_directives(parallel) - if (!is.null(parallel)) { - mark_openmp_used(scope) - } - error_check_after <- if (!is.null(parallel)) { - quickr_error_return_if_set( - scope, - openmp_depth = scope_openmp_depth(scope) - 1L - ) - } else { - "" - } loop_header <- glue("do {var_name} = {iterable}") Fortran(glue( - "{str_flatten_lines(directives$prefix, loop_header)} - {indent(body)} + "{str_flatten_lines(compiled$directives$prefix, loop_header)} + {indent(compiled$body)} end do - {str_flatten_lines(directives$suffix, error_check_after)} + {str_flatten_lines(compiled$directives$suffix, compiled$error_check_after)} " )) } diff --git a/R/r2f-iterables-helpers.R b/R/r2f-iterables-helpers.R index f77270f..cf00fbc 100644 --- a/R/r2f-iterables-helpers.R +++ b/R/r2f-iterables-helpers.R @@ -19,6 +19,35 @@ r2f_iterable_context <- function(calls) { } } +# Flatten a rank>1 value to a rank-1 vector, matching R's column-major +# drop-dims semantics for as.double()/as.integer()/as.vector() and +# matrix arguments to c(). Scalars and rank-1 values pass through +# unchanged. The canonical `reshape(source, [len])` spelling lives here +# so every flatten site shares it; the output length is +# value_length_expr(x@value) (NA -> a runtime size() query). The output +# mode is x's own mode -- callers cast before flattening if they want a +# different one. +# Used by: r2f-coercions.R, r2f-constructors.R +flatten_to_vector <- function(x, scope) { + stopifnot(inherits(x, Fortran)) + if (passes_as_scalar(x@value) || x@value@rank <= 1L) { + return(x) + } + len_expr <- value_length_expr(x@value) + len_str <- if (is_scalar_na(len_expr)) { + glue("size({x})") + } else { + # dims2f() returns "" for a scalar "1", but we need a literal length. + out <- dims2f(list(len_expr), scope) + if (!nzchar(out)) "1" else out + } + out_val <- Variable( + x@value@mode, + list(if (is_scalar_na(len_expr)) NA else len_expr) + ) + Fortran(glue("reshape({x}, [{len_str}])"), out_val) +} + # Compute the length expression for a Variable value. # Used by: r2f-sequences.R, r2f-control-flow.R value_length_expr <- function(value) { diff --git a/R/r2f-logical.R b/R/r2f-logical.R index 794fe5a..97af0ac 100644 --- a/R/r2f-logical.R +++ b/R/r2f-logical.R @@ -1,64 +1,8 @@ # r2f-logical.R -# Handlers for logical and comparison operators: !, &, &&, |, ||, >=, >, <, <=, ==, != +# Handlers for comparison and logical operators, plus is.null(). # --- Handlers --- -# ---- comparison operators ---- - -r2f_handlers[[">="]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} >= {right})"), var) -} - -r2f_handlers[[">"]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} > {right})"), var) -} - -r2f_handlers[["<"]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} < {right})"), var) -} - -r2f_handlers[["<="]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} <= {right})"), var) -} - -r2f_handlers[["=="]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} == {right})"), var) -} - -r2f_handlers[["!="]] <- function(args, scope, ...) { - .[left, right] <- lapply(args, r2f, scope, ...) - # R compares logicals as integers; Fortran has no logical comparison. - .[left, right] <- promote_arith_pair(left, right, "comparison") - var <- conform(left@value, right@value) - var@mode <- "logical" - Fortran(glue("({left} /= {right})"), var) -} - # ---- unary logical not ---- r2f_handlers[["!"]] <- function(args, scope, ...) { @@ -90,40 +34,280 @@ register_r2f_handler( } ) +lower_comparison_operands <- function(args, scope, op, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) + if ( + op %in% + c("<", "<=", ">", ">=") && + "complex" %in% c(left@value@mode, right@value@mode) + ) { + stop("invalid comparison with complex values", call. = FALSE) + } + .[left, right] <- promote_arith_pair(left, right, "comparison") + conform_elementwise_operands( + left, + right, + hoist, + scope, + scalarize_one_by_one = FALSE + ) +} -# ---- binary logical operators ---- +r2f_handlers[["<"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + "<", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} < {right})"), value) +} -# TODO: the scalar || probably need some more type checking. -# TODO: gfortran supports implicit casting that of logical to integer when -# assigning a logical to a variable declared integer, converting `.true.` to `1`, -# but this is not a standard language feature, and Intel's `ifort` uses `-1` for `.true`. -# We should explicitly use -# `merge(1_c_int, 0_c_int, )` to cast logical to int. -register_r2f_handler( - c("&", "&&", "|", "||"), - function(args, scope, ...) { - args <- lapply(args, r2f, scope, ...) - args <- lapply(args, function(a) { - if (a@value@mode != "logical") { - stop("must be logical") - } - a - }) - .[left, right] <- args - left <- booleanize_logical_as_int(left) - right <- booleanize_logical_as_int(right) - - operator <- switch( - last(list(...)$calls), - `&` = , - `&&` = ".and.", - `|` = , - `||` = ".or." +r2f_handlers[["<="]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + "<=", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} <= {right})"), value) +} + +r2f_handlers[[">"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + ">", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} > {right})"), value) +} + +r2f_handlers[[">="]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + ">=", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} >= {right})"), value) +} + +r2f_handlers[["=="]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + "==", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} == {right})"), value) +} + +r2f_handlers[["!="]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_comparison_operands( + args, + scope, + "!=", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("({left} /= {right})"), value) +} + +lower_logical_operands <- function(args, scope, op, ..., hoist = NULL) { + .[left, right] <- lower_elementwise_operands(args, scope, ..., hoist = hoist) + for (operand in list(left, right)) { + if (operand@value@mode != "logical") { + stop("`", op, "` requires logical operands", call. = FALSE) + } + } + left <- booleanize_logical_as_int(left) + right <- booleanize_logical_as_int(right) + .[left, right] <- conform_elementwise_operands( + left, + right, + hoist, + scope, + scalarize_one_by_one = FALSE + ) + list(left, right) +} + +r2f_handlers[["&"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_logical_operands( + args, + scope, + "&", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("{left} .and. {right}"), value) +} + +r2f_handlers[["|"]] <- function(args, scope, ..., hoist = NULL) { + .[left, right] <- lower_logical_operands( + args, + scope, + "|", + ..., + hoist = hoist + ) + value <- infer_result_variable(left@value, right@value) + value@mode <- "logical" + Fortran(glue("{left} .or. {right}"), value) +} + +# && and || are R's *scalar* control operators: operands must be length 1 +# (R errors otherwise), and the right operand is evaluated only when the +# left side does not already decide the answer. +andor_operand_is_length_one <- function(x) { + passes_as_scalar(x@value) || + x@value@rank > 0L && + all(vapply(x@value@dims, dim_is_one, logical(1L))) +} + +check_short_circuit_operand <- function(x, op) { + if (is.null(x@value) || !identical(x@value@mode, "logical")) { + stop("`", op, "` requires logical operands", call. = FALSE) + } + if (!andor_operand_is_length_one(x)) { + stop( + "`", + op, + "` requires length-1 operands; use `", + if (op == "&&") "&" else "|", + "` for elementwise operations", + call. = FALSE ) + } + invisible(TRUE) +} - s <- glue("{left} {operator} {right}") - val <- conform(left@value, right@value) - val@mode <- "logical" - Fortran(s, val) +scalarize_andor_operand <- function(x, op, hoist) { + check_short_circuit_operand(x, op) + if (passes_as_scalar(x@value)) { + return(booleanize_logical_as_int(x)) } -) + if (is.null(hoist)) { + stop("internal error: `", op, "` requires hoist context", call. = FALSE) + } + + if (isTRUE(x@logical_booleanized)) { + tmp <- hoist$declare_tmp(mode = "logical", dims = x@value@dims) + hoist$emit(glue("{tmp@name} = {x}")) + x <- Fortran(tmp@name, tmp) + } else { + x <- hoist_unless_name(x, hoist) + } + idxs <- rep("1", x@value@rank) + Fortran( + glue("{x}({str_flatten_commas(idxs)})"), + Variable("logical") + ) +} + +# TRUE when evaluating `e` eagerly is indistinguishable from R's lazy +# right-operand evaluation: no side effects, no errors, no traps. A +# conservative whitelist -- names, literals, and compositions of pure +# non-trapping operations. Anything else (subscripts, %%/%/%, function +# calls, ...) gets the conditional lowering. +is_pure_scalar_condition <- function(e, scope) { + if (is.symbol(e) || (is.atomic(e) && length(e) == 1L)) { + return(TRUE) + } + if (!is.call(e) || !is.symbol(e[[1L]])) { + return(FALSE) + } + op <- as.character(e[[1L]]) + pure_ops <- c( + "(", + "!", + "&&", + "||", + "&", + "|", + "<", + "<=", + ">", + ">=", + "==", + "!=", + "+", + "-", + "*", + "/", + "abs" + ) + if (!op %in% pure_ops) { + return(FALSE) + } + if (inherits(scope[[op]], LocalClosure)) { + return(FALSE) + } + all(vapply( + as.list(e)[-1L], + is_pure_scalar_condition, + logical(1L), + scope = scope + )) +} + +lower_short_circuit_operator <- function(args, scope, op, ..., hoist = NULL) { + stopifnot(length(args) == 2L, op %in% c("&&", "||")) + + left <- r2f(args[[1L]], scope, ..., hoist = hoist) + left <- scalarize_andor_operand(left, op, hoist) + + f <- if (op == "&&") ".and." else ".or." + + if (is_pure_scalar_condition(args[[2L]], scope)) { + # Fortran may evaluate both operands of .and./.or.; for a pure right + # operand that is indistinguishable from short-circuiting, so keep + # the compact infix form. + right <- r2f(args[[2L]], scope, ..., hoist = hoist) + right <- scalarize_andor_operand(right, op, hoist) + return(Fortran(glue("{left} {f} {right}"), Variable("logical"))) + } + + if (is.null(hoist)) { + stop("internal error: `", op, "` requires hoist context", call. = FALSE) + } + sub <- new_hoist(scope) + right <- r2f(args[[2L]], scope, ..., hoist = sub) + right <- scalarize_andor_operand(right, op, sub) + + tmp <- hoist$declare_tmp(mode = "logical", dims = NULL) + hoist$emit(glue("{tmp@name} = {left}")) + condition <- if (op == "&&") tmp@name else glue(".not. {tmp@name}") + hoist$emit(glue("if ({condition}) then")) + hoist$emit(indent(sub$render(glue("{tmp@name} = {right}")))) + hoist$emit("end if") + Fortran(tmp@name, tmp) +} + +r2f_handlers[["&&"]] <- function(args, scope, ..., hoist = NULL) { + lower_short_circuit_operator(args, scope, "&&", ..., hoist = hoist) +} + +r2f_handlers[["||"]] <- function(args, scope, ..., hoist = NULL) { + lower_short_circuit_operator(args, scope, "||", ..., hoist = hoist) +} diff --git a/R/r2f-math.R b/R/r2f-math.R index b69fb60..926bad0 100644 --- a/R/r2f-math.R +++ b/R/r2f-math.R @@ -55,15 +55,9 @@ r2f_handlers[["floor"]] <- function(args, scope, ..., hoist = NULL) { } out_val <- Variable("double", arg@value@dims) - # Avoid Fortran FLOOR() overflow (it returns an integer) by staying in the - # real domain: - # - aint(x) truncates toward 0 (real result) - # - adjust by -1 where trunc differs from floor (negative non-integers) - aint <- glue("aint({arg})") - Fortran( - glue("({aint} - merge(1.0_c_double, 0.0_c_double, ({arg} < {aint})))"), - out_val - ) + # Avoid Fortran FLOOR() overflow (it returns an integer) by staying in + # the real domain; real_floor_expr() shares the spelling with `%/%`. + Fortran(real_floor_expr(arg), out_val) } r2f_handlers[["ceiling"]] <- function(args, scope, ..., hoist = NULL) { diff --git a/R/r2f-matrix-blas.R b/R/r2f-matrix-blas.R index 46d2719..8f0c63a 100644 --- a/R/r2f-matrix-blas.R +++ b/R/r2f-matrix-blas.R @@ -38,40 +38,34 @@ assert_rank_leq2 <- function(x, message) { } # Assert right-hand side rank is vector or matrix. -assert_rhs_rank <- function( - rank, - err_scalar, - err_high, - call_scalar = FALSE, - call_high = FALSE -) { - stopifnot( - is_wholenumber(rank), - is_string(err_scalar), - is_string(err_high), - is_bool(call_scalar), - is_bool(call_high) - ) +assert_vector_or_matrix_rhs <- function(rank, err_scalar, err_high) { + stopifnot(is_wholenumber(rank), is_string(err_scalar), is_string(err_high)) if (rank > 2L) { - stop(err_high, call. = call_high) + stop(err_high, call. = FALSE) } if (rank == 0L) { - stop(err_scalar, call. = call_scalar) + stop(err_scalar, call. = FALSE) } invisible(TRUE) } -# Assert conformability and warn on unknown. -assert_conformable_dims <- function(left, right, context, err_msg) { - stopifnot(is_string(context), is_string(err_msg)) - conform <- check_conformable(left, right) - if (!conform$ok) { - stop(err_msg, call. = FALSE) +# BLAS/LAPACK dimensions use equality semantics: equal zero contracted +# dimensions are conformable and can still produce a non-empty result. +check_blas_dims <- function(left, right) { + if (is_wholenumber(left) && is_wholenumber(right)) { + return(list( + ok = identical(as.integer(left), as.integer(right)), + unknown = FALSE + )) } - if (conform$unknown) { - warn_conformability_unknown(left, right, context) + if (!is_scalar_na(left) && !is_scalar_na(right)) { + left_norm <- fortranize_expr_symbols(left) + right_norm <- fortranize_expr_symbols(right) + if (identical(left_norm, right_norm)) { + return(list(ok = TRUE, unknown = FALSE)) + } } - invisible(TRUE) + list(ok = TRUE, unknown = TRUE) } # Return the R symbol name if operand is a bare symbol; otherwise NULL. @@ -164,72 +158,112 @@ effective_dims <- function(dims, trans) { } } -# Return conformability status (ok/unknown) without side-effects. -check_conformable <- function(left, right) { - if (is_wholenumber(left) && is_wholenumber(right)) { - ok <- identical(as.integer(left), as.integer(right)) - return(list(ok = ok, unknown = FALSE)) +# Effective operand and result shapes for %*%. `left_dims`/`right_dims` +# come from matrix_dims*() with vectors oriented as a row (left) or +# column (right) vector; transposes apply to matrix operands only (a +# transposed vector is already reoriented by its dims). The result is +# left_eff$rows x right_eff$cols in every case -- the gemv cases keep +# their literal 1 extent from the vector orientation. Shared by the +# %*% handler and infer_dest_matmul() so lowering and dest inference +# cannot drift. +matmul_shapes <- function( + left_rank, + left_dims, + left_trans, + right_rank, + right_dims, + right_trans +) { + left_eff <- if (left_rank == 2L) { + effective_dims(left_dims, left_trans) + } else { + left_dims } - if (identical(left, right)) { - return(list(ok = TRUE, unknown = FALSE)) + right_eff <- if (right_rank == 2L) { + effective_dims(right_dims, right_trans) + } else { + right_dims } - list(ok = TRUE, unknown = TRUE) -} - -warn_conformability_unknown <- function(left, right, context) { - left_txt <- if (is.null(left)) "NULL" else deparse(left) - right_txt <- if (is.null(right)) "NULL" else deparse(right) - warning( - "cannot verify conformability in ", - context, - " at compile time: ", - left_txt, - " vs ", - right_txt, - call. = FALSE + list( + left_eff = left_eff, + right_eff = right_eff, + out_dims = list(left_eff$rows, right_eff$cols) ) - invisible(FALSE) } -# Assert that dimensions represent a square matrix (rows == cols). -# Throws an error if dimensions are known to be non-conformable, -# and warns if conformability cannot be verified at compile time. -assert_square_matrix <- function(rows, cols, context) { - conform <- check_conformable(rows, cols) - if (!conform$ok) { - stop(context, " requires a square matrix", call. = FALSE) - } - if (conform$unknown) { - warn_conformability_unknown(rows, cols, context) - } - invisible(TRUE) +# Enforce that `dims` describe a square matrix: a known mismatch is a +# compile error; unverifiable dims get a runtime guard on the operand's +# actual extents. +assert_square_matrix <- function(dims, operand, context, hoist, scope) { + guard_conformable_dims( + dims$rows, + dims$cols, + paste0(context, " requires a square matrix"), + hoist, + scope, + left = operand, + right = operand, + left_axis = 1L, + right_axis = 2L, + checker = check_blas_dims + ) } # ---- BLAS emitters ---- -# Check that destination dimensions match expected output dimensions. -assert_dest_dims_compatible <- function(dest, expected_dims, context) { - if (is.null(dest) || is.null(expected_dims)) { +# Reject zero output extents where GEMM/GEMV/SYRK/DGER would receive an invalid +# leading dimension. A zero contracted dimension remains supported when every +# output extent is nonzero. +assert_nonempty_blas_output <- function( + dim, + operand, + axis, + context, + hoist, + scope +) { + stopifnot( + inherits(operand, Fortran), + is.numeric(axis), + length(axis) == 1L, + is_string(context) + ) + message <- paste0(context, " zero-sized outputs are not supported") + if (is_wholenumber(dim)) { + if (as.integer(dim) == 0L) { + stop(message, call. = FALSE) + } return(invisible(TRUE)) } - expected_rank <- length(expected_dims) - if (dest@rank != expected_rank) { - stop("assignment target has incompatible rank for ", context, call. = FALSE) + + emit_quickr_error_if( + glue("{dimension_guard_expr(dim, operand, axis)} == 0_c_ptrdiff_t"), + message, + hoist, + scope + ) + invisible(TRUE) +} + +# TRUE when the destination's declared shape is *proven* to match the +# expected output shape: rank equal and every extent proven equal +# (dims_proven_equal()). Anything unproven -- symbolic dims that merely +# fail to be refuted, NA dims -- is FALSE: the emitter then routes +# through a hoisted temporary and the assignment shape check guards (or +# refuses) the copy. Accepting an unproven dest passed wrong leading +# dimensions to the BLAS call and could write past the allocation. +dest_dims_proven_equal <- function(dest, expected_dims) { + if (is.null(expected_dims)) { + return(FALSE) } - for (i in seq_len(expected_rank)) { - dest_dim <- dest@dims[[i]] - expected_dim <- expected_dims[[i]] - if (is_wholenumber(dest_dim) && is_wholenumber(expected_dim)) { - if (!identical(as.integer(dest_dim), as.integer(expected_dim))) { - stop( - "assignment target has incompatible dimensions for ", - context, - call. = FALSE - ) - } - } + if (dest@rank != length(expected_dims)) { + return(FALSE) } - invisible(TRUE) + all(vapply( + seq_along(expected_dims), + function(i) dims_proven_equal(dest@dims[[i]], expected_dims[[i]]), + logical(1) + )) } # Determine if output can safely write into dest without aliasing. @@ -255,7 +289,9 @@ can_use_output <- function( if (!identical(logical_as_int(dest), logical_is_c_int)) { return(FALSE) } - assert_dest_dims_compatible(dest, expected_dims, context) + if (!dest_dims_proven_equal(dest, expected_dims)) { + return(FALSE) + } output_name <- dest@name if (is.null(output_name) || !nzchar(output_name)) { return(FALSE) @@ -270,6 +306,89 @@ can_use_output <- function( !output_name %in% disallowed } +# Resolve where a BLAS/LAPACK emitter writes its result: the assignment +# destination when can_use_output() allows it, otherwise a hoisted +# temporary declared with the expected dims. Returns list(var, name, +# use_dest); wrap up with finalize_blas_output(). +resolve_blas_output <- function( + dest, + hoist, + input_names, + expected_dims, + context, + allow_alias = character(), + mode = "double", + logical_is_c_int = FALSE +) { + if ( + can_use_output( + dest, + input_names = input_names, + expected_dims = expected_dims, + context = context, + allow_alias = allow_alias, + mode = mode, + logical_is_c_int = logical_is_c_int + ) + ) { + return(list(var = dest, name = dest@name, use_dest = TRUE)) + } + var <- hoist$declare_tmp( + mode = mode, + dims = expected_dims, + logical_as_int = logical_is_c_int + ) + list(var = var, name = var@name, use_dest = FALSE) +} + +# Wrap a resolved output as the emitter's return value, marking +# destination writes so the assignment handler skips the copy. +finalize_blas_output <- function(out) { + f <- Fortran(out$name, out$var) + if (out$use_dest) { + f@writes_to_dest <- TRUE + } + f +} + +# Emit the guard pair for a LAPACK `info` result: a routine-specific +# message when info > 0 and the uniform illegal-argument message when +# info < 0. dgesdd checks the negative case first; the per-site order is +# preserved so the emitted guards (and snapshots) are unchanged. +emit_lapack_info_guards <- function( + info, + routine, + positive_msg, + hoist, + scope, + negative_first = FALSE +) { + emit_positive <- function() { + emit_quickr_error_if( + condition = glue("{info} > 0_c_int"), + message = positive_msg, + hoist = hoist, + scope = scope + ) + } + emit_negative <- function() { + emit_quickr_error_if( + condition = glue("{info} < 0_c_int"), + message = glue("Lapack routine {routine}: illegal argument"), + hoist = hoist, + scope = scope + ) + } + if (negative_first) { + emit_negative() + emit_positive() + } else { + emit_positive() + emit_negative() + } + invisible(TRUE) +} + # Ensure a BLAS operand is named, hoisting into a temp if needed. ensure_blas_operand_name <- function(x, hoist) { name <- symbol_name_or_null(x) @@ -288,7 +407,7 @@ ensure_blas_operand_name <- function(x, hoist) { # Wrap an expression as a BLAS int literal. blas_int <- function(x) { x_str <- if (is.language(x)) { - gsub("([0-9]+)L\\b", "\\1", deparse1(x)) + gsub("([0-9]+)L\\b", "\\1", deparse1(fortranize_size_calls(x))) } else if (is_wholenumber(x)) { as.character(as.integer(x)) } else { @@ -297,7 +416,32 @@ blas_int <- function(x) { glue("int({x_str}, kind=c_int)") } -# Centralized GEMM emission with optional destination +# Emit a BLAS call for positive contractions and fill the result with zero +# without calling BLAS when the contracted dimension is zero. +emit_blas_contraction <- function(call, output, contracted_dim, hoist) { + stopifnot(is_string(call), is_string(output)) + assert_hoist_env(hoist) + + if (is_wholenumber(contracted_dim)) { + if (as.integer(contracted_dim) == 0L) { + hoist$emit(glue("{output} = 0.0_c_double")) + } else { + hoist$emit(call) + } + return(invisible(TRUE)) + } + + hoist$emit(glue( + " +if ({blas_int(contracted_dim)} == 0_c_int) then + {output} = 0.0_c_double +else + {call} +end if" + )) + invisible(TRUE) +} + # gemm: centralized BLAS GEMM emission. # - 'hoist' is required and provided by r2f(); handlers thread it through so # helpers can pre-emit temporary assignments and BLAS calls. @@ -318,34 +462,40 @@ gemm <- function( context = "gemm" ) { assert_hoist_env(hoist) + assert_nonempty_blas_output( + m, + left, + if (opA == "N") 1L else 2L, + context, + hoist, + scope + ) + assert_nonempty_blas_output( + n, + right, + if (opB == "N") 2L else 1L, + context, + hoist, + scope + ) A_name <- ensure_blas_operand_name(left, hoist) B_name <- ensure_blas_operand_name(right, hoist) - if ( - can_use_output( - dest, - input_names = c(A_name, B_name), - expected_dims = list(m, n), - context = context - ) - ) { - hoist$emit(glue( - "call dgemm('{opA}','{opB}', {blas_int(m)}, {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {B_name}, {blas_int(ldb)}, 0.0_c_double, {dest@name}, {blas_int(ldc_expr)})" - )) - out <- Fortran(dest@name, dest) - out@writes_to_dest <- TRUE - return(out) - } - - output_var <- hoist$declare_tmp(mode = "double", dims = list(m, n)) - hoist$emit(glue( - "call dgemm('{opA}','{opB}', {blas_int(m)}, {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {B_name}, {blas_int(ldb)}, 0.0_c_double, {output_var@name}, {blas_int(ldc_expr)})" - )) - Fortran(output_var@name, output_var) + out <- resolve_blas_output( + dest, + hoist, + input_names = c(A_name, B_name), + expected_dims = list(m, n), + context = context + ) + blas_call <- glue( + "call dgemm('{opA}','{opB}', {blas_int(m)}, {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {B_name}, {blas_int(ldb)}, 0.0_c_double, {out$name}, {blas_int(ldc_expr)})" + ) + emit_blas_contraction(blas_call, out$name, k, hoist) + finalize_blas_output(out) } -# Centralized GEMV emission with optional destination -# gemv: centralized BLAS GEMV emission. +# gemv: centralized BLAS GEMV emission with optional destination. # - 'hoist' is required and provided by r2f(); handlers thread it through so # helpers can pre-emit temporary assignments and BLAS calls. gemv <- function( @@ -362,39 +512,39 @@ gemv <- function( context = "gemv" ) { assert_hoist_env(hoist) + output_dim <- if (transA == "N") m else n + assert_nonempty_blas_output( + output_dim, + A, + if (transA == "N") 1L else 2L, + context, + hoist, + scope + ) A_name <- ensure_blas_operand_name(A, hoist) x_name <- ensure_blas_operand_name(x, hoist) - if ( - can_use_output( - dest, - input_names = c(A_name, x_name), - expected_dims = out_dims, - context = context - ) - ) { - # Assign output to output destination - hoist$emit(glue( - "call dgemv('{transA}', {blas_int(m)}, {blas_int(n)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {x_name}, 1_c_int, 0.0_c_double, {dest@name}, 1_c_int)" - )) - out <- Fortran(dest@name, dest) - out@writes_to_dest <- TRUE - return(out) - } - # Else assign to a temporary variable - output_var <- hoist$declare_tmp(mode = "double", dims = out_dims) - hoist$emit(glue( - "call dgemv('{transA}', {blas_int(m)}, {blas_int(n)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {x_name}, 1_c_int, 0.0_c_double, {output_var@name}, 1_c_int)" - )) - Fortran(output_var@name, output_var) + out <- resolve_blas_output( + dest, + hoist, + input_names = c(A_name, x_name), + expected_dims = out_dims, + context = context + ) + blas_call <- glue( + "call dgemv('{transA}', {blas_int(m)}, {blas_int(n)}, 1.0_c_double, {A_name}, {blas_int(lda)}, {x_name}, 1_c_int, 0.0_c_double, {out$name}, 1_c_int)" + ) + contracted_dim <- if (transA == "N") n else m + emit_blas_contraction(blas_call, out$name, contracted_dim, hoist) + finalize_blas_output(out) } symmetrize_upper_to_lower <- function(target, n, hoist) { stopifnot(is_string(target)) assert_hoist_env(hoist) - idx_i <- hoist$declare_tmp(mode = "integer", dims = list(1L)) - idx_j <- hoist$declare_tmp(mode = "integer", dims = list(1L)) + idx_i <- hoist$declare_tmp(mode = "integer", dims = NULL) + idx_j <- hoist$declare_tmp(mode = "integer", dims = NULL) n_int <- blas_int(n) hoist$emit(glue( " @@ -449,8 +599,6 @@ syrk <- function( context = "syrk" ) { assert_hoist_env(hoist) - X_name <- ensure_blas_operand_name(X, hoist) - x_dims <- matrix_dims(X) # For trans = "T": C = t(X) %*% X, so C is k x k where k = ncol(X) @@ -463,38 +611,32 @@ syrk <- function( k <- x_dims$cols } lda <- x_dims$rows + assert_nonempty_blas_output( + n, + X, + if (trans == "T") 2L else 1L, + context, + hoist, + scope + ) + X_name <- ensure_blas_operand_name(X, hoist) # Output is symmetric n x n matrix - writes_to_dest <- FALSE - out_var <- NULL - out_name <- NULL - - if ( - can_use_output( - dest, - input_names = X_name, - expected_dims = list(n, n), - context = context - ) - ) { - writes_to_dest <- TRUE - out_var <- dest - out_name <- dest@name - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = list(n, n)) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = X_name, + expected_dims = list(n, n), + context = context + ) - hoist$emit(glue( - "call dsyrk('U', '{trans}', {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {X_name}, {blas_int(lda)}, 0.0_c_double, {out_name}, {blas_int(n)})" - )) - symmetrize_upper_to_lower(out_name, n, hoist = hoist) + blas_call <- glue( + "call dsyrk('U', '{trans}', {blas_int(n)}, {blas_int(k)}, 1.0_c_double, {X_name}, {blas_int(lda)}, 0.0_c_double, {out$name}, {blas_int(n)})" + ) + emit_blas_contraction(blas_call, out$name, k, hoist) + symmetrize_upper_to_lower(out$name, n, hoist = hoist) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } # Emit BLAS outer product for vectors or scalars with optional destination. @@ -508,8 +650,8 @@ outer_mul <- function( ) { assert_hoist_env(hoist) - x <- maybe_cast_double(x) - y <- maybe_cast_double(y) + x <- cast_linalg_double(x, context) + y <- cast_linalg_double(y, context) if (x@value@rank > 1L || y@value@rank > 1L) { stop("outer() only supports vectors or scalars") @@ -518,32 +660,24 @@ outer_mul <- function( m <- dim_or_one(x, 1L) n <- dim_or_one(y, 1L) + assert_nonempty_blas_output(m, x, 1L, context, hoist, scope) + assert_nonempty_blas_output(n, y, 1L, context, hoist, scope) + x_name <- ensure_blas_operand_name(x, hoist) y_name <- ensure_blas_operand_name(y, hoist) - if ( - can_use_output( - dest, - input_names = c(x_name, y_name), - expected_dims = list(m, n), - context = context - ) - ) { - hoist$emit(glue("{dest@name} = 0.0_c_double")) - hoist$emit(glue( - "call dger({blas_int(m)}, {blas_int(n)}, 1.0_c_double, {x_name}, 1_c_int, {y_name}, 1_c_int, {dest@name}, {blas_int(m)})" - )) - out <- Fortran(dest@name, dest) - out@writes_to_dest <- TRUE - return(out) - } - - output_var <- hoist$declare_tmp(mode = "double", dims = list(m, n)) - hoist$emit(glue("{output_var@name} = 0.0_c_double")) + out <- resolve_blas_output( + dest, + hoist, + input_names = c(x_name, y_name), + expected_dims = list(m, n), + context = context + ) + hoist$emit(glue("{out$name} = 0.0_c_double")) hoist$emit(glue( - "call dger({blas_int(m)}, {blas_int(n)}, 1.0_c_double, {x_name}, 1_c_int, {y_name}, 1_c_int, {output_var@name}, {blas_int(m)})" + "call dger({blas_int(m)}, {blas_int(n)}, 1.0_c_double, {x_name}, 1_c_int, {y_name}, 1_c_int, {out$name}, {blas_int(m)})" )) - Fortran(output_var@name, output_var) + finalize_blas_output(out) } # Emit triangular solve (vector or matrix RHS) with optional destination. @@ -560,70 +694,50 @@ triangular_solve <- function( ) { assert_hoist_env(hoist) - A <- maybe_cast_double(A) - B <- maybe_cast_double(B) + A <- cast_linalg_double(A, context) + B <- cast_linalg_double(B, context) assert_rank2_matrix(A, "triangular solve expects a matrix") a_dims <- matrix_dims(A) - conform <- check_conformable(a_dims$rows, a_dims$cols) - if (!conform$ok) { - stop("non-conformable arguments in triangular solve", call. = FALSE) - } - if (conform$unknown) { - warn_conformability_unknown(a_dims$rows, a_dims$cols, "triangular solve") - } + assert_square_matrix(a_dims, A, "triangular solve", hoist, scope) n <- a_dims$rows b_rank <- B@value@rank - assert_rhs_rank( + assert_vector_or_matrix_rhs( b_rank, err_scalar = "triangular solve expects a vector or matrix right-hand side", err_high = "triangular solve only supports vector or matrix right-hand sides" ) - if (b_rank == 1L) { - b_len <- dim_or_one(B, 1L) - assert_conformable_dims( - n, - b_len, - context = "triangular solve", - err_msg = "non-conformable arguments in triangular solve" - ) - } else { - b_rows <- dim_or_one(B, 1L) - assert_conformable_dims( - n, - b_rows, - context = "triangular solve", - err_msg = "non-conformable arguments in triangular solve" - ) - } + guard_conformable_dims( + n, + dim_or_one(B, 1L), + "non-conformable arguments in triangular solve", + hoist, + scope, + left = A, + right = B, + left_axis = 1L, + right_axis = if (b_rank == 1L) NULL else 1L, + checker = check_blas_dims + ) A_name <- ensure_blas_operand_name(A, hoist) B_input_name <- symbol_name_or_null(B) - if ( - can_use_output( - dest, - input_names = c(A_name, B_input_name), - expected_dims = B@value@dims, - context = context, - allow_alias = B_input_name - ) - ) { - hoist$emit(glue("{dest@name} = {B}")) - B_name <- dest@name - out_var <- dest - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp( - mode = B@value@mode %||% "double", - dims = B@value@dims - ) - hoist$emit(glue("{out_var@name} = {B}")) - B_name <- out_var@name - writes_to_dest <- FALSE - } + # The solve routines overwrite their right-hand side, so the output + # (dest or temp) doubles as the B argument after copying B into it. + out <- resolve_blas_output( + dest, + hoist, + input_names = c(A_name, B_input_name), + expected_dims = B@value@dims, + context = context, + allow_alias = setdiff(B_input_name, A_name), + mode = B@value@mode %||% "double" + ) + hoist$emit(glue("{out$name} = {B}")) + B_name <- out$name if (b_rank <= 1L) { hoist$emit(glue( @@ -636,11 +750,7 @@ triangular_solve <- function( )) } - out <- Fortran(B_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } lapack_solve <- function( @@ -654,8 +764,8 @@ lapack_solve <- function( ) { assert_hoist_env(hoist) - A <- maybe_cast_double(A) - B <- maybe_cast_double(B) + A <- cast_linalg_double(A, context) + B <- cast_linalg_double(B, context) assert_rank2_matrix(A, paste0(context, " expects a matrix for `a`")) @@ -664,220 +774,194 @@ lapack_solve <- function( n <- a_dims$cols b_rank <- B@value@rank - assert_rhs_rank( + assert_vector_or_matrix_rhs( b_rank, err_scalar = paste0(context, " expects a vector or matrix right-hand side"), err_high = paste0( context, " only supports vector or matrix right-hand sides" - ), - call_scalar = FALSE, - call_high = FALSE + ) ) - if (b_rank == 1L) { - b_len <- dim_or_one(B, 1L) - assert_conformable_dims( - m, - b_len, - context = context, - err_msg = paste0("non-conformable arguments in ", context) - ) - } else { - b_rows <- dim_or_one(B, 1L) - assert_conformable_dims( - m, - b_rows, - context = context, - err_msg = paste0("non-conformable arguments in ", context) - ) - } + guard_conformable_dims( + m, + dim_or_one(B, 1L), + paste0("non-conformable arguments in ", context), + hoist, + scope, + left = A, + right = B, + left_axis = 1L, + right_axis = if (b_rank == 1L) NULL else 1L, + checker = check_blas_dims + ) A_name <- ensure_blas_operand_name(A, hoist) B_input_name <- ensure_blas_operand_name(B, hoist) nrhs <- if (b_rank == 1L) 1L else dim_or_one(B, 2L) - square <- check_conformable(m, n) - if (square$ok && !square$unknown && !identical(context, "qr.solve")) { - A_work <- hoist$declare_tmp(mode = "double", dims = list(m, m)) - hoist$emit(glue("{A_work@name} = {A_name}")) - - expected_dims <- if (b_rank == 1L) list(n) else list(n, nrhs) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = c(A_name, B_input_name), - expected_dims = expected_dims, - context = context, - allow_alias = B_input_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = expected_dims) - out_name <- out_var@name - } - hoist$emit(glue("{out_name} = {B_input_name}")) - - ipiv <- hoist$declare_tmp(mode = "integer", dims = list(m)) - info <- hoist$declare_tmp(mode = "integer", dims = NULL) + # Both lowerings write a solution shaped by R's contract: length follows + # ncol(a), width follows the right-hand side. Each lowering resolves the + # output target at its own write point (declaration order matters for + # the emitted block) via resolve_blas_output(). + expected_dims <- if (b_rank == 1L) list(n) else list(n, nrhs) - hoist$emit(glue( - "call dgesv({blas_int(m)}, {blas_int(nrhs)}, {A_work@name}, {blas_int(m)}, {ipiv@name}, {out_name}, {blas_int(m)}, {info@name})" - )) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dgesv: system is exactly singular", + if (identical(context, "qr.solve")) { + lapack_solve_qr( + A_name = A_name, + B_input_name = B_input_name, + m = m, + n = n, + nrhs = nrhs, + b_rank = b_rank, + expected_dims = expected_dims, + dest = dest, + context = context, + tol = tol, hoist = hoist, scope = scope ) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dgesv: illegal argument", + } else { + lapack_solve_gesv( + A = A, + a_dims = a_dims, + A_name = A_name, + B = B, + B_input_name = B_input_name, + m = m, + nrhs = nrhs, + b_rank = b_rank, + expected_dims = expected_dims, + dest = dest, + context = context, hoist = hoist, scope = scope ) - - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - return(out) } +} - if (identical(context, "qr.solve")) { - A_work <- hoist$declare_tmp(mode = "double", dims = list(m, n)) - hoist$emit(glue("{A_work@name} = {A_name}")) - - B_work <- hoist$declare_tmp(mode = "double", dims = list(m, nrhs)) - m_f <- dims2f(list(m), scope) - if (!nzchar(m_f)) { - m_f <- "1" - } - nrhs_f <- dims2f(list(nrhs), scope) - if (!nzchar(nrhs_f)) { - nrhs_f <- "1" - } - hoist$emit(glue("{B_work@name} = 0.0_c_double")) - if (b_rank == 1L) { - hoist$emit(glue("{B_work@name}(1:{m_f}, 1) = {B_input_name}")) +# Square solve via dgesv. R's solve() requires a square `a`; least +# squares is qr.solve()'s job. Statically rectangular `a` is a compile +# error, symbolic dims get a runtime guard before the dgesv call. (A +# rectangular `a` used to fall through to a dgels least-squares solve -- +# an answer where R errors.) +lapack_solve_gesv <- function( + A, + a_dims, + A_name, + B, + B_input_name, + m, + nrhs, + b_rank, + expected_dims, + dest, + context, + hoist, + scope +) { + assert_square_matrix(a_dims, A, context, hoist, scope) + if (b_rank == 2L) { + message <- "no right-hand side in 'b'" + if (is_wholenumber(nrhs)) { + if (as.integer(nrhs) == 0L) { + stop(message, call. = FALSE) + } } else { - hoist$emit(glue("{B_work@name}(1:{m_f}, 1:{nrhs_f}) = {B_input_name}")) + emit_quickr_error_if( + condition = glue( + "{dimension_guard_expr(nrhs, B, 2L)} == 0_c_ptrdiff_t" + ), + message = message, + hoist = hoist, + scope = scope + ) } + } + A_work <- hoist$declare_tmp(mode = "double", dims = list(m, m)) + hoist$emit(glue("{A_work@name} = {A_name}")) - qraux <- hoist$declare_tmp(mode = "double", dims = list(n)) - jpvt <- hoist$declare_tmp(mode = "integer", dims = list(n)) - work <- hoist$declare_tmp(mode = "double", dims = list(n, 2L)) - rank <- hoist$declare_tmp(mode = "integer", dims = NULL) - idx <- hoist$declare_tmp(mode = "integer", dims = NULL) - - hoist$emit(glue( - " -do {idx@name} = 1_c_int, {blas_int(n)} - {jpvt@name}({idx@name}) = {idx@name} -end do" - )) - - tol_value <- if (is.null(tol)) "1e-7_c_double" else as.character(tol) - mn <- call("min", m, n) - hoist$emit(glue( - "call dqrdc2({A_work@name}, {blas_int(m)}, {blas_int(m)}, {blas_int(n)}, {tol_value}, {rank@name}, {qraux@name}, {jpvt@name}, {work@name})" - )) - - emit_quickr_error_if( - condition = glue("{rank@name} < {blas_int(mn)}"), - message = "rank deficient matrix in qr.solve", - hoist = hoist, - scope = scope - ) - - coef_work <- hoist$declare_tmp( - mode = "double", - dims = list(mn, nrhs) - ) - hoist$emit(glue("{coef_work@name} = 0.0_c_double")) - info <- hoist$declare_tmp(mode = "integer", dims = NULL) + out <- resolve_blas_output( + dest, + hoist, + input_names = c(A_name, B_input_name), + expected_dims = expected_dims, + context = context, + allow_alias = B_input_name + ) + # The output length follows ncol(a) (R's contract) while `b` follows + # nrow(a); the two are only runtime-equal. When ncol is statically 1 + # the output declares as a scalar, so a symbolic-length `b` must be + # copied elementwise, not by whole-array assignment. + b_src <- if (passes_as_scalar(out$var) && !passes_as_scalar(B@value)) { + subs <- str_flatten_commas(rep("1", b_rank)) + glue("{B_input_name}({subs})") + } else { + B_input_name + } + hoist$emit(glue("{out$name} = {b_src}")) - hoist$emit(glue( - "call dqrcf({A_work@name}, {blas_int(m)}, {rank@name}, {qraux@name}, {B_work@name}, {blas_int(nrhs)}, {coef_work@name}, {info@name})" - )) - emit_quickr_error_if( - condition = glue("{info@name} /= 0_c_int"), - message = "exact singularity in 'qr.coef'", - hoist = hoist, - scope = scope - ) + ipiv <- hoist$declare_tmp(mode = "integer", dims = list(m)) + info <- hoist$declare_tmp(mode = "integer", dims = NULL) - expected_dims <- if (b_rank == 1L) list(n) else list(n, nrhs) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = c(A_name, B_input_name), - expected_dims = expected_dims, - context = context, - allow_alias = B_input_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = expected_dims) - out_name <- out_var@name - } + hoist$emit(glue( + "call dgesv({blas_int(m)}, {blas_int(nrhs)}, {A_work@name}, {blas_int(m)}, {ipiv@name}, {out$name}, {blas_int(m)}, {info@name})" + )) + emit_lapack_info_guards( + info@name, + "dgesv", + "Lapack routine dgesv: system is exactly singular", + hoist, + scope + ) + finalize_blas_output(out) +} - if (passes_as_scalar(out_var)) { - hoist$emit(glue("{out_name} = {coef_work@name}(1, 1)")) - } else { - hoist$emit(glue("{out_name} = 0.0_c_double")) - if (b_rank == 1L) { - idx <- hoist$declare_tmp(mode = "integer", dims = NULL) - hoist$emit(glue( - " -do {idx@name} = 1_c_int, {rank@name} - {out_name}({jpvt@name}({idx@name})) = {coef_work@name}({idx@name}, 1) -end do" - )) - } else { - idx_i <- hoist$declare_tmp(mode = "integer", dims = NULL) - idx_j <- hoist$declare_tmp(mode = "integer", dims = NULL) - hoist$emit(glue( - " -do {idx_j@name} = 1_c_int, {blas_int(nrhs)} - do {idx_i@name} = 1_c_int, {rank@name} - {out_name}({jpvt@name}({idx_i@name}), {idx_j@name}) = {coef_work@name}({idx_i@name}, {idx_j@name}) - end do -end do" - )) +# Least-squares solve via the LINPACK dqrdc2/dqrcf pair (R's own qr() +# routines), permuting the rank-truncated coefficients back through the +# pivot vector. +lapack_solve_qr <- function( + A_name, + B_input_name, + m, + n, + nrhs, + b_rank, + expected_dims, + dest, + context, + tol, + hoist, + scope +) { + design_message <- "qr.solve coefficient matrices with zero extents are not supported" + for (axis in 1:2) { + dim <- list(m, n)[[axis]] + if (is_wholenumber(dim)) { + if (as.integer(dim) == 0L) { + stop(design_message, call. = FALSE) } + } else { + emit_quickr_error_if( + condition = glue( + "size({A_name}, {axis}, kind=c_ptrdiff_t) == 0_c_ptrdiff_t" + ), + message = design_message, + hoist = hoist, + scope = scope + ) } - - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - return(out) } A_work <- hoist$declare_tmp(mode = "double", dims = list(m, n)) hoist$emit(glue("{A_work@name} = {A_name}")) - max_mn <- call("max", m, n) - - B_work <- hoist$declare_tmp(mode = "double", dims = list(max_mn, nrhs)) + B_work <- hoist$declare_tmp(mode = "double", dims = list(m, nrhs)) m_f <- dims2f(list(m), scope) if (!nzchar(m_f)) { m_f <- "1" } - n_f <- dims2f(list(n), scope) - if (!nzchar(n_f)) { - n_f <- "1" - } nrhs_f <- dims2f(list(nrhs), scope) if (!nzchar(nrhs_f)) { nrhs_f <- "1" @@ -889,226 +973,188 @@ end do" hoist$emit(glue("{B_work@name}(1:{m_f}, 1:{nrhs_f}) = {B_input_name}")) } - info <- hoist$declare_tmp(mode = "integer", dims = NULL) + qraux <- hoist$declare_tmp(mode = "double", dims = list(n)) + jpvt <- hoist$declare_tmp(mode = "integer", dims = list(n)) + work <- hoist$declare_tmp(mode = "double", dims = list(n, 2L)) + rank <- hoist$declare_tmp(mode = "integer", dims = NULL) + idx <- hoist$declare_tmp(mode = "integer", dims = NULL) - mn <- call("min", m, n) - if (identical(context, "qr.solve")) { - jpvt <- hoist$declare_tmp(mode = "integer", dims = list(n)) - hoist$emit(glue("{jpvt@name} = 0_c_int")) - - rcond <- if (is.null(tol)) "1e-7_c_double" else as.character(tol) - rank <- hoist$declare_tmp(mode = "integer", dims = NULL) - - lwork <- call( - "max", - 1L, - call("+", mn, call("max", mn, nrhs)), - call("+", call("*", 2L, mn), call("*", 64L, call("+", n, 1L))), - call("+", mn, call("*", 2L, n)) - ) - work <- hoist$declare_tmp(mode = "double", dims = list(lwork)) + hoist$emit(glue( + " +do {idx@name} = 1_c_int, {blas_int(n)} + {jpvt@name}({idx@name}) = {idx@name} +end do" + )) - hoist$emit(glue( - "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})" - )) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dgelsy: illegal argument", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dgelsy failed to converge", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{rank@name} < {blas_int(n)}"), - message = "rank deficient matrix in qr.solve", - hoist = hoist, - scope = scope - ) - } else { - lwork <- call("max", 1L, call("+", mn, call("max", mn, nrhs))) - work <- hoist$declare_tmp(mode = "double", dims = list(lwork)) + tol_value <- if (is.null(tol)) "1e-7_c_double" else as.character(tol) + mn <- diag_length_expr(m, n, context) + hoist$emit(glue( + "call dqrdc2({A_work@name}, {blas_int(m)}, {blas_int(m)}, {blas_int(n)}, {tol_value}, {rank@name}, {qraux@name}, {jpvt@name}, {work@name})" + )) - hoist$emit(glue( - "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})" + emit_quickr_error_if( + condition = glue("{rank@name} < {blas_int(mn)}"), + message = "rank deficient matrix in qr.solve", + hoist = hoist, + scope = scope + ) + + coef_work <- hoist$declare_tmp( + mode = "double", + dims = list(mn, nrhs) + ) + hoist$emit(glue("{coef_work@name} = 0.0_c_double")) + + emit_dqrcf <- function(target, info) { + target$emit(glue( + "call dqrcf({A_work@name}, {blas_int(m)}, {rank@name}, {qraux@name}, {B_work@name}, {blas_int(nrhs)}, {coef_work@name}, {info@name})" )) emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dgels: illegal argument", - hoist = hoist, + condition = glue("{info@name} /= 0_c_int"), + message = "exact singularity in 'qr.coef'", + hoist = target, scope = scope ) } - - expected_dims <- if (b_rank == 1L) list(n) else list(n, nrhs) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = c(A_name, B_input_name), - expected_dims = expected_dims, - context = context, - allow_alias = B_input_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE + if (is_wholenumber(nrhs)) { + if (as.integer(nrhs) > 0L) { + info <- hoist$declare_tmp(mode = "integer", dims = NULL) + emit_dqrcf(hoist, info) + } } else { - out_var <- hoist$declare_tmp(mode = "double", dims = expected_dims) - out_name <- out_var@name + info <- hoist$declare_tmp(mode = "integer", dims = NULL) + sub <- new_hoist(scope) + emit_dqrcf(sub, info) + hoist$emit(glue("if ({blas_int(nrhs)} > 0_c_int) then")) + hoist$emit(indent(sub$render(character()))) + hoist$emit("end if") } - if (b_rank == 1L) { - if (passes_as_scalar(out_var)) { - hoist$emit(glue("{out_name} = {B_work@name}(1, 1)")) + out <- resolve_blas_output( + dest, + hoist, + input_names = c(A_name, B_input_name), + expected_dims = expected_dims, + context = context, + allow_alias = B_input_name + ) + + if (passes_as_scalar(out$var)) { + hoist$emit(glue("{out$name} = {coef_work@name}(1, 1)")) + } else { + hoist$emit(glue("{out$name} = 0.0_c_double")) + if (b_rank == 1L) { + idx <- hoist$declare_tmp(mode = "integer", dims = NULL) + hoist$emit(glue( + " +do {idx@name} = 1_c_int, {rank@name} + {out$name}({jpvt@name}({idx@name})) = {coef_work@name}({idx@name}, 1) +end do" + )) } else { - hoist$emit(glue("{out_name} = {B_work@name}(1:{n_f}, 1)")) + idx_i <- hoist$declare_tmp(mode = "integer", dims = NULL) + idx_j <- hoist$declare_tmp(mode = "integer", dims = NULL) + hoist$emit(glue( + " +do {idx_j@name} = 1_c_int, {blas_int(nrhs)} + do {idx_i@name} = 1_c_int, {rank@name} + {out$name}({jpvt@name}({idx_i@name}), {idx_j@name}) = {coef_work@name}({idx_i@name}, {idx_j@name}) + end do +end do" + )) } - } else { - hoist$emit(glue("{out_name} = {B_work@name}(1:{n_f}, 1:{nrhs_f})")) } - - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } lapack_inverse <- function(A, scope, hoist, dest = NULL, context = "solve") { assert_hoist_env(hoist) - A <- maybe_cast_double(A) + A <- cast_linalg_double(A, context) assert_rank2_matrix(A, paste0(context, " expects a matrix for `a`")) a_dims <- matrix_dims(A) - assert_square_matrix(a_dims$rows, a_dims$cols, context) + assert_square_matrix(a_dims, A, context, hoist, scope) n <- a_dims$rows A_name <- ensure_blas_operand_name(A, hoist) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = A_name, - expected_dims = list(n, n), - context = context, - allow_alias = A_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = list(n, n)) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = A_name, + expected_dims = list(n, n), + context = context, + allow_alias = A_name + ) - hoist$emit(glue("{out_name} = {A_name}")) + hoist$emit(glue("{out$name} = {A_name}")) ipiv <- hoist$declare_tmp(mode = "integer", dims = list(n)) info <- hoist$declare_tmp(mode = "integer", dims = NULL) work <- hoist$declare_tmp(mode = "double", dims = list(n)) hoist$emit(glue( - "call dgetrf({blas_int(n)}, {blas_int(n)}, {out_name}, {blas_int(n)}, {ipiv@name}, {info@name})" + "call dgetrf({blas_int(n)}, {blas_int(n)}, {out$name}, {blas_int(n)}, {ipiv@name}, {info@name})" )) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dgetrf: system is exactly singular", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dgetrf: illegal argument", - hoist = hoist, - scope = scope + emit_lapack_info_guards( + info@name, + "dgetrf", + "Lapack routine dgetrf: system is exactly singular", + hoist, + scope ) hoist$emit(glue( - "call dgetri({blas_int(n)}, {out_name}, {blas_int(n)}, {ipiv@name}, {work@name}, {blas_int(n)}, {info@name})" + "call dgetri({blas_int(n)}, {out$name}, {blas_int(n)}, {ipiv@name}, {work@name}, {blas_int(n)}, {info@name})" )) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dgetri: system is exactly singular", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dgetri: illegal argument", - hoist = hoist, - scope = scope + emit_lapack_info_guards( + info@name, + "dgetri", + "Lapack routine dgetri: system is exactly singular", + hoist, + scope ) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } lapack_chol <- function(A, scope, hoist, dest = NULL, context = "chol") { assert_hoist_env(hoist) - A <- maybe_cast_double(A) + A <- cast_linalg_double(A, context) assert_rank2_matrix(A, paste0(context, " expects a matrix")) a_dims <- matrix_dims(A) - assert_square_matrix(a_dims$rows, a_dims$cols, context) + assert_square_matrix(a_dims, A, context, hoist, scope) n <- a_dims$rows A_name <- ensure_blas_operand_name(A, hoist) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = A_name, - expected_dims = list(n, n), - context = context, - allow_alias = A_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = list(n, n)) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = A_name, + expected_dims = list(n, n), + context = context, + allow_alias = A_name + ) - hoist$emit(glue("{out_name} = {A_name}")) + hoist$emit(glue("{out$name} = {A_name}")) info <- hoist$declare_tmp(mode = "integer", dims = NULL) hoist$emit(glue( - "call dpotrf('U', {blas_int(n)}, {out_name}, {blas_int(n)}, {info@name})" + "call dpotrf('U', {blas_int(n)}, {out$name}, {blas_int(n)}, {info@name})" )) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dpotrf: leading minor is not positive definite", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dpotrf: illegal argument", - hoist = hoist, - scope = scope + emit_lapack_info_guards( + info@name, + "dpotrf", + "Lapack routine dpotrf: leading minor is not positive definite", + hoist, + scope ) - zero_lower_triangle(out_name, n, hoist = hoist) + zero_lower_triangle(out$name, n, hoist = hoist) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } lapack_chol2inv <- function( @@ -1120,58 +1166,40 @@ lapack_chol2inv <- function( ) { assert_hoist_env(hoist) - R <- maybe_cast_double(R) + R <- cast_linalg_double(R, context) assert_rank2_matrix(R, paste0(context, " expects a matrix")) r_dims <- matrix_dims(R) - assert_square_matrix(r_dims$rows, r_dims$cols, context) + assert_square_matrix(r_dims, R, context, hoist, scope) n <- r_dims$rows R_name <- ensure_blas_operand_name(R, hoist) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = R_name, - expected_dims = list(n, n), - context = context, - allow_alias = R_name - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp(mode = "double", dims = list(n, n)) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = R_name, + expected_dims = list(n, n), + context = context, + allow_alias = R_name + ) - hoist$emit(glue("{out_name} = {R_name}")) + hoist$emit(glue("{out$name} = {R_name}")) info <- hoist$declare_tmp(mode = "integer", dims = NULL) hoist$emit(glue( - "call dpotri('U', {blas_int(n)}, {out_name}, {blas_int(n)}, {info@name})" + "call dpotri('U', {blas_int(n)}, {out$name}, {blas_int(n)}, {info@name})" )) - emit_quickr_error_if( - condition = glue("{info@name} > 0_c_int"), - message = "Lapack routine dpotri: matrix is not positive definite", - hoist = hoist, - scope = scope - ) - emit_quickr_error_if( - condition = glue("{info@name} < 0_c_int"), - message = "Lapack routine dpotri: illegal argument", - hoist = hoist, - scope = scope + emit_lapack_info_guards( + info@name, + "dpotri", + "Lapack routine dpotri: matrix is not positive definite", + hoist, + scope ) - symmetrize_upper_to_lower(out_name, n, hoist = hoist) + symmetrize_upper_to_lower(out$name, n, hoist = hoist) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } diag_extract <- function(x, scope, hoist, dest = NULL, context = "diag") { @@ -1187,42 +1215,25 @@ diag_extract <- function(x, scope, hoist, dest = NULL, context = "diag") { x_name <- ensure_blas_operand_name(x, hoist) logical_is_c_int <- logical_as_int(x@value) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = x_name, - expected_dims = list(diag_len), - context = context, - mode = x@value@mode, - logical_is_c_int = logical_is_c_int - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp( - mode = x@value@mode, - dims = list(diag_len), - logical_as_int = logical_is_c_int - ) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = x_name, + expected_dims = list(diag_len), + context = context, + mode = x@value@mode, + logical_is_c_int = logical_is_c_int + ) idx_i <- hoist$declare_tmp(mode = "integer", dims = NULL) hoist$emit(glue( " do {idx_i@name} = 1_c_int, {blas_int(diag_len)} - {out_name}({idx_i@name}) = {x_name}({idx_i@name}, {idx_i@name}) + {out$name}({idx_i@name}) = {x_name}({idx_i@name}, {idx_i@name}) end do" )) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } diag_matrix <- function( @@ -1249,38 +1260,25 @@ diag_matrix <- function( x_name <- ensure_blas_operand_name(x, hoist) - writes_to_dest <- FALSE - if ( - can_use_output( - dest, - input_names = x_name, - expected_dims = list(nrow, ncol), - context = context, - mode = mode, - logical_is_c_int = logical_is_c_int - ) - ) { - out_var <- dest - out_name <- dest@name - writes_to_dest <- TRUE - } else { - out_var <- hoist$declare_tmp( - mode = mode, - dims = list(nrow, ncol), - logical_as_int = logical_is_c_int - ) - out_name <- out_var@name - } + out <- resolve_blas_output( + dest, + hoist, + input_names = x_name, + expected_dims = list(nrow, ncol), + context = context, + mode = mode, + logical_is_c_int = logical_is_c_int + ) zero <- switch( mode, double = "0.0_c_double", integer = "0_c_int", - logical = if (logical_as_int(out_var)) "0_c_int" else ".false.", + logical = if (logical_as_int(out$var)) "0_c_int" else ".false.", complex = "(0.0_c_double, 0.0_c_double)", stop(context, " does not support mode ", mode, call. = FALSE) ) - hoist$emit(glue("{out_name} = {zero}")) + hoist$emit(glue("{out$name} = {zero}")) idx_i <- hoist$declare_tmp(mode = "integer", dims = NULL) value_expr <- if (x_scalar) { @@ -1295,15 +1293,11 @@ diag_matrix <- function( hoist$emit(glue( " do {idx_i@name} = 1_c_int, {blas_int(diag_len)} - {out_name}({idx_i@name}, {idx_i@name}) = {value_expr} + {out$name}({idx_i@name}, {idx_i@name}) = {value_expr} end do" )) - out <- Fortran(out_name, out_var) - if (writes_to_dest) { - out@writes_to_dest <- TRUE - } - out + finalize_blas_output(out) } svd_dims <- function(A, context = "svd") { @@ -1332,7 +1326,7 @@ lapack_svd <- function( assert_hoist_env(hoist) stopifnot(inherits(d, Variable), inherits(u, Variable), inherits(v, Variable)) - A <- maybe_cast_double(A) + A <- cast_linalg_double(A, context) dims <- svd_dims(A, context = context) m <- dims$m n <- dims$n @@ -1346,6 +1340,9 @@ lapack_svd <- function( info <- hoist$declare_tmp(mode = "integer", dims = NULL) lwork <- hoist$declare_tmp(mode = "integer", dims = NULL) + # dims list(1L) is quickr's *scalar* spelling (see Variable@is_scalar), + # but the work query must be a length-1 array so `work_query(1)` is + # subscriptable; the unfoldable `1 + 0` keeps the array declaration. work_query <- hoist$declare_tmp( mode = "double", dims = list(call("+", 1L, 0L)) @@ -1368,17 +1365,13 @@ lapack_svd <- function( hoist$emit(glue( "call dgesdd('S', {blas_int(m)}, {blas_int(n)}, {A_work@name}, {blas_int(m)}, {d@name}, {u@name}, {blas_int(m)}, {vt@name}, {blas_int(mn)}, {work@name}, {lwork@name}, {iwork@name}, {info@name})" )) - emit_quickr_error_if( - glue("{info@name} < 0_c_int"), - "Lapack routine dgesdd: illegal argument", - hoist, - scope - ) - emit_quickr_error_if( - glue("{info@name} > 0_c_int"), + emit_lapack_info_guards( + info@name, + "dgesdd", "Lapack routine dgesdd failed to converge", hoist, - scope + scope, + negative_first = TRUE ) hoist$emit(glue("{v@name} = transpose({vt@name})")) diff --git a/R/r2f-matrix-infer.R b/R/r2f-matrix-infer.R index e4eba9f..295b4f8 100644 --- a/R/r2f-matrix-infer.R +++ b/R/r2f-matrix-infer.R @@ -72,28 +72,15 @@ infer_dest_matmul <- function(args, scope) { orientation = if (right_rank == 1L) "colvec" else "matrix" ) - left_eff <- if (left_rank == 2L) { - effective_dims(left_dims, left_trans) - } else { - left_dims - } - right_eff <- if (right_rank == 2L) { - effective_dims(right_dims, right_trans) - } else { - right_dims - } - - if (left_rank == 2L && right_rank == 1L) { - out_len <- if (left_trans == "N") left_dims$rows else left_dims$cols - return(Variable("double", list(out_len, 1L))) - } - if (left_rank == 1L && right_rank == 2L) { - transA <- if (right_trans == "N") "T" else "N" - out_len <- if (transA == "N") right_dims$rows else right_dims$cols - return(Variable("double", list(1L, out_len))) - } - - Variable("double", list(left_eff$rows, right_eff$cols)) + shapes <- matmul_shapes( + left_rank, + left_dims, + left_trans, + right_rank, + right_dims, + right_trans + ) + Variable("double", shapes$out_dims) } # Shared inference for crossprod/tcrossprod destination sizes. @@ -200,7 +187,8 @@ infer_dest_solve <- function(args, scope) { NULL } -# Infer destination dimensions for chol(). +# Infer destination dimensions for chol() and chol2inv(): both return a +# square double matrix shaped like `x`. infer_dest_chol <- function(args, scope) { x_arg <- args$x %||% args[[1L]] if (is.null(x_arg)) { @@ -214,19 +202,7 @@ infer_dest_chol <- function(args, scope) { Variable("double", list(x_dims$rows, x_dims$cols)) } -# Infer destination dimensions for chol2inv(). -infer_dest_chol2inv <- function(args, scope) { - x_arg <- args$x %||% args[[1L]] - if (is.null(x_arg)) { - return(NULL) - } - X <- infer_symbol_var(x_arg, scope) - if (is.null(X) || X@rank != 2L) { - return(NULL) - } - x_dims <- matrix_dims_var(X) - Variable("double", list(x_dims$rows, x_dims$cols)) -} +infer_dest_chol2inv <- infer_dest_chol # Helper to infer a size from a literal or symbol. infer_size <- function(arg, scope) { @@ -255,10 +231,11 @@ infer_size <- function(arg, scope) { NULL } -# Infer destination dimensions for diag(). -infer_dest_diag <- function(args, scope) { - # R signature: diag(x = 1, nrow, ncol, names = TRUE) - # Handle both named and positional arguments +# Match diag()'s x/nrow/ncol arguments, named or positional. +# R signature: diag(x = 1, nrow, ncol, names = TRUE). Shared by the +# diag() handler and infer_dest_diag() so lowering and dest inference +# cannot drift. The returned `x` is never the missing arg (NULL instead). +diag_call_args <- function(args) { arg_names <- names(args) if (is.null(arg_names)) { arg_names <- rep("", length(args)) @@ -288,11 +265,26 @@ infer_dest_diag <- function(args, scope) { ncol_arg <- args[[unnamed_idx[[3L]]]] } - has_nrow <- !is.null(nrow_arg) && !is_missing(nrow_arg) - has_ncol <- !is.null(ncol_arg) && !is_missing(ncol_arg) + list( + x = x_arg, + nrow = nrow_arg, + ncol = ncol_arg, + has_nrow = !is.null(nrow_arg) && !is_missing(nrow_arg), + has_ncol = !is.null(ncol_arg) && !is_missing(ncol_arg) + ) +} + +# Infer destination dimensions for diag(). +infer_dest_diag <- function(args, scope) { + margs <- diag_call_args(args) + x_arg <- margs$x + nrow_arg <- margs$nrow + ncol_arg <- margs$ncol + has_nrow <- margs$has_nrow + has_ncol <- margs$has_ncol # Case: no x, just nrow (identity matrix) - if (is.null(x_arg) || is_missing(x_arg)) { + if (is.null(x_arg)) { if (!has_nrow) { return(NULL) } @@ -333,8 +325,11 @@ infer_dest_diag <- function(args, scope) { } } - # Case: x is a scalar symbol without nrow/ncol (identity matrix) - if (!is.null(x) && x@rank == 0L && !has_nrow && !has_ncol) { + # Case: x is a length-1 symbol without nrow/ncol (identity matrix). Must + # match the handler's predicate exactly, or the inferred destination + # would be the 1x1 constructor result instead of the identity's (x, x). + # The size depends on x's value, so leave the destination uninferred. + if (!is.null(x) && passes_as_scalar(x) && !has_nrow && !has_ncol) { return(NULL) } diff --git a/R/r2f-matrix-parse.R b/R/r2f-matrix-parse.R index d492f0f..f242bd6 100644 --- a/R/r2f-matrix-parse.R +++ b/R/r2f-matrix-parse.R @@ -9,7 +9,7 @@ unwrap_transpose_arg <- function(arg, scope, ..., hoist) { if (is_call(arg_unwrapped, quote(t)) && length(arg_unwrapped) == 2L) { inner_arg <- unwrap_parens(arg_unwrapped[[2L]]) inner <- r2f(inner_arg, scope, ..., hoist = hoist) - inner <- maybe_cast_double(inner) + inner <- cast_linalg_double(inner, "%*%") if (inner@value@rank == 2L) { return(list(value = inner, trans = "T")) } else if (inner@value@rank == 1L) { @@ -26,7 +26,7 @@ unwrap_transpose_arg <- function(arg, scope, ..., hoist) { } } value <- r2f(arg, scope, ..., hoist = hoist) - value <- maybe_cast_double(value) + value <- cast_linalg_double(value, "%*%") list(value = value, trans = "N") } diff --git a/R/r2f-matrix.R b/R/r2f-matrix.R index 6615f46..5406260 100644 --- a/R/r2f-matrix.R +++ b/R/r2f-matrix.R @@ -29,16 +29,16 @@ register_r2f_handler( orientation = if (right_rank == 1) "colvec" else "matrix" ) - left_eff <- if (left_rank == 2) { - effective_dims(left_dims, left_trans) - } else { - left_dims - } - right_eff <- if (right_rank == 2) { - effective_dims(right_dims, right_trans) - } else { - right_dims - } + shapes <- matmul_shapes( + left_rank, + left_dims, + left_trans, + right_rank, + right_dims, + right_trans + ) + left_eff <- shapes$left_eff + right_eff <- shapes$right_eff # Compute effective shapes m <- left_eff$rows @@ -52,15 +52,17 @@ register_r2f_handler( # Matrix-Vector: use GEMV if (left_rank == 2 && right_rank == 1) { - expected_len <- if (left_trans == "N") left_dims$cols else left_dims$rows - conform <- check_conformable(expected_len, right_dims$rows) - if (!conform$ok) { - stop("non-conformable arguments in %*%", call. = FALSE) - } - if (conform$unknown) { - warn_conformability_unknown(expected_len, right_dims$rows, "%*%") - } - out_len <- if (left_trans == "N") left_dims$rows else left_dims$cols + guard_conformable_dims( + left_eff$cols, + right_dims$rows, + "non-conformable arguments in %*%", + hoist, + scope, + left = left, + right = right, + left_axis = if (left_trans == "N") 2L else 1L, + checker = check_blas_dims + ) return(gemv( transA = left_trans, A = left, @@ -68,7 +70,7 @@ register_r2f_handler( m = left_dims$rows, n = left_dims$cols, lda = left_dims$rows, - out_dims = list(out_len, 1L), + out_dims = shapes$out_dims, scope = scope, hoist = hoist, dest = dest, @@ -78,15 +80,17 @@ register_r2f_handler( # Vector-Matrix: use GEMV with transpose if (left_rank == 1 && right_rank == 2) { transA <- if (right_trans == "N") "T" else "N" - expected_len <- if (transA == "N") right_dims$cols else right_dims$rows - conform <- check_conformable(left_dims$cols, expected_len) - if (!conform$ok) { - stop("non-conformable arguments in %*%", call. = FALSE) - } - if (conform$unknown) { - warn_conformability_unknown(left_dims$cols, expected_len, "%*%") - } - out_len <- if (transA == "N") right_dims$rows else right_dims$cols + guard_conformable_dims( + left_dims$cols, + right_eff$rows, + "non-conformable arguments in %*%", + hoist, + scope, + left = left, + right = right, + right_axis = if (transA == "N") 2L else 1L, + checker = check_blas_dims + ) return(gemv( transA = transA, A = right, @@ -94,7 +98,7 @@ register_r2f_handler( m = right_dims$rows, n = right_dims$cols, lda = right_dims$rows, - out_dims = list(1L, out_len), + out_dims = shapes$out_dims, scope = scope, hoist = hoist, dest = dest, @@ -102,13 +106,32 @@ register_r2f_handler( )) } - conform <- check_conformable(k, right_eff$rows) - if (!conform$ok) { - stop("non-conformable arguments in %*%", call. = FALSE) - } - if (conform$unknown) { - warn_conformability_unknown(k, right_eff$rows, "%*%") - } + # Vector operands (vector %*% vector reaches here) are rank-1: their + # extent is their whole size, not a rank-2 axis. + guard_conformable_dims( + k, + right_eff$rows, + "non-conformable arguments in %*%", + hoist, + scope, + left = left, + right = right, + left_axis = if (left_rank == 1) { + NULL + } else if (left_trans == "N") { + 2L + } else { + 1L + }, + right_axis = if (right_rank == 1) { + NULL + } else if (right_trans == "N") { + 1L + } else { + 2L + }, + checker = check_blas_dims + ) # Matrix-Matrix gemm( @@ -191,6 +214,9 @@ register_r2f_handler( } ) +# Join the input modes for cbind/rbind. Hand-rolled rather than +# reduce_promoted_mode(): binds also support "raw" (not on the mode +# lattice) and refuse mixing complex with other modes. bind_output_mode <- function(values, context) { modes <- unique(vapply( values, @@ -244,6 +270,9 @@ bind_dim_sum <- function(values, context, label) { reduce(values, \(a, b) call("+", a, b)) } +# Unlike the BLAS conformability checks, unknown dims here stay a compile +# error: the common dim is needed to declare the cbind/rbind output, and a +# runtime guard cannot conjure a declaration. bind_common_dim <- function(dim_list, scalar_flags, context, label) { non_scalar <- which(!scalar_flags) if (!length(non_scalar)) { @@ -261,17 +290,10 @@ bind_common_dim <- function(dim_list, scalar_flags, context, label) { } if (length(non_scalar) > 1L) { for (idx in non_scalar[-1L]) { - conform <- check_conformable(target, dim_list[[idx]]) - if (!conform$ok) { - stop( - context, - " requires inputs with a common ", - label, - " count", - call. = FALSE - ) - } - if (conform$unknown) { + # A dim that is not provably equal to the common one is an error + # either way: the declaration needs the dim, so "unknown" cannot be + # deferred to a runtime guard here. + if (!dims_match(target, dim_list[[idx]])) { stop( context, " requires inputs with a common ", @@ -293,7 +315,7 @@ bind_dim_string <- function(dim) { } else if (is.numeric(dim)) { as.character(dim) } else { - gsub("([0-9]+)L\\b", "\\1", deparse1(dim)) + gsub("([0-9]+)L\\b", "\\1", deparse1(fortranize_size_calls(dim))) } } @@ -301,14 +323,22 @@ bind_dim_int <- function(dim) { paste0("int(", bind_dim_string(dim), ")") } -bind_col_matrix_expr <- function(value, rows, is_scalar, context) { - rows_int <- bind_dim_int(rows) +# Reshape one cbind/rbind input to a matrix piece along the bind axis: +# scalars spread to the common dim, vectors reshape to a single column +# (cbind) or row (rbind), matrices pass through. +bind_piece_expr <- function(value, common, is_scalar, context, direction) { + common_int <- bind_dim_int(common) + shape <- if (direction == "cbind") { + glue("[{common_int}, 1]") + } else { + glue("[1, {common_int}]") + } if (is_scalar) { - vec <- glue("spread({value}, 1, {rows_int})") - return(glue("reshape({vec}, [{rows_int}, 1])")) + vec <- glue("spread({value}, 1, {common_int})") + return(glue("reshape({vec}, {shape})")) } if (value@value@rank == 1L) { - return(glue("reshape({value}, [{rows_int}, 1])")) + return(glue("reshape({value}, {shape})")) } if (value@value@rank == 2L) { return(as.character(value)) @@ -316,135 +346,84 @@ bind_col_matrix_expr <- function(value, rows, is_scalar, context) { stop(context, " only supports rank 0-2 inputs", call. = FALSE) } -bind_row_matrix_expr <- function(value, cols, is_scalar, context) { - cols_int <- bind_dim_int(cols) - if (is_scalar) { - vec <- glue("spread({value}, 1, {cols_int})") - return(glue("reshape({vec}, [1, {cols_int}])")) - } - if (value@value@rank == 1L) { - return(glue("reshape({value}, [1, {cols_int}])")) +# cbind()/rbind() share one skeleton: clean the args, join input modes, +# require a known common dim (bind_common_dim) and sum the other, then +# assemble the pieces. Only the orientation differs; rbind builds the +# transpose and flips it at the end so the array constructor still fills +# column-major. +compile_bind <- function(args, scope, ..., hoist = NULL) { + direction <- last(list(...)$calls) + context <- paste0(direction, "()") + + if (!is.null(args$deparse.level) && !is_missing(args$deparse.level)) { + args$deparse.level <- NULL } - if (value@value@rank == 2L) { - return(as.character(value)) + args <- args[!vapply(args, is_missing, logical(1))] + args <- args[ + !vapply( + args, + \(x) is.null(x) || identical(x, quote(NULL)), + logical(1) + ) + ] + if (!length(args)) { + stop(context, " requires at least one argument", call. = FALSE) } - stop(context, " only supports rank 0-2 inputs", call. = FALSE) -} - -register_r2f_handler( - "cbind", - function(args, scope, ..., hoist = NULL) { - context <- "cbind()" - if (!is.null(args$deparse.level) && !is_missing(args$deparse.level)) { - args$deparse.level <- NULL - } - args <- args[!vapply(args, is_missing, logical(1))] - args <- args[ - !vapply( - args, - \(x) is.null(x) || identical(x, quote(NULL)), - logical(1) - ) - ] - if (!length(args)) { - stop("cbind() requires at least one argument", call. = FALSE) - } - values <- lapply(args, r2f, scope, ..., hoist = hoist) - for (val in values) { - if (is.null(val@value) || is.null(val@value@mode)) { - stop(context, " inputs must have a value", call. = FALSE) - } - assert_rank_leq2(val, paste0(context, " only supports rank 0-2 inputs")) + values <- lapply(args, r2f, scope, ..., hoist = hoist) + for (val in values) { + if (is.null(val@value) || is.null(val@value@mode)) { + stop(context, " inputs must have a value", call. = FALSE) } + assert_rank_leq2(val, paste0(context, " only supports rank 0-2 inputs")) + } - mode <- bind_output_mode(values, context) - values <- lapply(values, cast_to_mode, mode = mode, context = context) + mode <- bind_output_mode(values, context) + values <- lapply(values, cast_to_mode, mode = mode, context = context) - dims <- lapply(values, matrix_dims, orientation = "colvec") - scalar_flags <- map_lgl(values, \(val) passes_as_scalar(val@value)) - row_sizes <- lapply(dims, `[[`, "rows") - col_sizes <- lapply(dims, `[[`, "cols") + orientation <- if (direction == "cbind") "colvec" else "rowvec" + dims <- lapply(values, matrix_dims, orientation = orientation) + scalar_flags <- map_lgl(values, \(val) passes_as_scalar(val@value)) + row_sizes <- lapply(dims, `[[`, "rows") + col_sizes <- lapply(dims, `[[`, "cols") - rows <- bind_common_dim(row_sizes, scalar_flags, context, "row") + if (direction == "cbind") { + common <- bind_common_dim(row_sizes, scalar_flags, context, "row") + rows <- common cols <- bind_dim_sum(col_sizes, context, "column") + } else { + common <- bind_common_dim(col_sizes, scalar_flags, context, "column") + cols <- common + rows <- bind_dim_sum(row_sizes, context, "row") + } - col_exprs <- vector("list", length(values)) - for (i in seq_along(values)) { - col_exprs[[i]] <- bind_col_matrix_expr( - value = values[[i]], - rows = rows, - is_scalar = scalar_flags[[i]], - context = context - ) - } - - data_expr <- glue("[{str_flatten_commas(col_exprs)}]") - out_expr <- glue( - "reshape({data_expr}, [{bind_dim_int(rows)}, {bind_dim_int(cols)}])" + pieces <- vector("list", length(values)) + for (i in seq_along(values)) { + pieces[[i]] <- bind_piece_expr( + value = values[[i]], + common = common, + is_scalar = scalar_flags[[i]], + context = context, + direction = direction ) - Fortran(out_expr, Variable(mode, list(rows, cols))) } -) - -register_r2f_handler( - "rbind", - function(args, scope, ..., hoist = NULL) { - context <- "rbind()" - if (!is.null(args$deparse.level) && !is_missing(args$deparse.level)) { - args$deparse.level <- NULL - } - args <- args[!vapply(args, is_missing, logical(1))] - args <- args[ - !vapply( - args, - \(x) is.null(x) || identical(x, quote(NULL)), - logical(1) - ) - ] - if (!length(args)) { - stop("rbind() requires at least one argument", call. = FALSE) - } - - values <- lapply(args, r2f, scope, ..., hoist = hoist) - for (val in values) { - if (is.null(val@value) || is.null(val@value@mode)) { - stop(context, " inputs must have a value", call. = FALSE) - } - assert_rank_leq2(val, paste0(context, " only supports rank 0-2 inputs")) - } - - mode <- bind_output_mode(values, context) - values <- lapply(values, cast_to_mode, mode = mode, context = context) - - dims <- lapply(values, matrix_dims, orientation = "rowvec") - scalar_flags <- map_lgl(values, \(val) passes_as_scalar(val@value)) - row_sizes <- lapply(dims, `[[`, "rows") - col_sizes <- lapply(dims, `[[`, "cols") - - cols <- bind_common_dim(col_sizes, scalar_flags, context, "column") - rows <- bind_dim_sum(row_sizes, context, "row") - - row_exprs <- vector("list", length(values)) - for (i in seq_along(values)) { - row_exprs[[i]] <- bind_row_matrix_expr( - value = values[[i]], - cols = cols, - is_scalar = scalar_flags[[i]], - context = context - ) - } - transposed <- lapply(row_exprs, \(expr) glue("transpose({expr})")) + out_expr <- if (direction == "cbind") { + data_expr <- glue("[{str_flatten_commas(pieces)}]") + glue("reshape({data_expr}, [{bind_dim_int(rows)}, {bind_dim_int(cols)}])") + } else { + transposed <- lapply(pieces, \(expr) glue("transpose({expr})")) data_expr <- glue("[{str_flatten_commas(transposed)}]") combined <- glue( "reshape({data_expr}, [{bind_dim_int(cols)}, {bind_dim_int(rows)}])" ) - out_expr <- glue("transpose({combined})") - - Fortran(out_expr, Variable(mode, list(rows, cols))) + glue("transpose({combined})") } -) + + Fortran(out_expr, Variable(mode, list(rows, cols))) +} + +register_r2f_handler(c("cbind", "rbind"), compile_bind) # Handle crossprod(), using SYRK for single-arg and GEMM for two-arg forms. @@ -460,9 +439,7 @@ register_r2f_handler( ..., hoist = hoist, dest = dest, - trans_single = "T", - opA = "T", - opB = "N", + trans = "T", context = "crossprod" ) }, @@ -484,9 +461,7 @@ register_r2f_handler( ..., hoist = hoist, dest = dest, - trans_single = "N", - opA = "N", - opB = "T", + trans = "N", context = "tcrossprod" ) }, @@ -607,7 +582,10 @@ register_r2f_handler( tol <- if (is.null(tol_arg) || is_missing(tol_arg)) { r2f(1e-7, scope, ..., hoist = hoist) } else { - tol <- maybe_cast_double(r2f(tol_arg, scope, ..., hoist = hoist)) + tol <- cast_linalg_double( + r2f(tol_arg, scope, ..., hoist = hoist), + "qr.solve" + ) if (tol@value@rank != 0L) { stop("qr.solve() expects a scalar `tol`", call. = FALSE) } @@ -682,49 +660,19 @@ register_r2f_handler( register_r2f_handler( "diag", function(args, scope, ..., hoist = NULL, dest = NULL) { - # R signature: diag(x = 1, nrow, ncol, names = TRUE) - # Handle both named and positional arguments - arg_names <- names(args) - if (is.null(arg_names)) { - arg_names <- rep("", length(args)) - } - unnamed_idx <- which(!nzchar(arg_names) | is.na(arg_names)) - - # Extract x (named or position 1) - x_arg <- NULL - if (!is.null(args$x) && !is_missing(args$x)) { - x_arg <- args$x - } else if (length(unnamed_idx) >= 1L) { - candidate <- args[[unnamed_idx[[1L]]]] - if (!is_missing(candidate)) { - x_arg <- candidate - } - } - - # Extract nrow (named or position 2) - nrow_arg <- args$nrow - if (is.null(nrow_arg) && length(unnamed_idx) >= 2L) { - nrow_arg <- args[[unnamed_idx[[2L]]]] - } - - # Extract ncol (named or position 3) - ncol_arg <- args$ncol - if (is.null(ncol_arg) && length(unnamed_idx) >= 3L) { - ncol_arg <- args[[unnamed_idx[[3L]]]] - } + margs <- diag_call_args(args) + x_arg <- margs$x + nrow_arg <- margs$nrow + ncol_arg <- margs$ncol + has_nrow <- margs$has_nrow + has_ncol <- margs$has_ncol if (!is.null(args$names) && !is_missing(args$names)) { logical_arg_or_default(args, "names", TRUE, "diag()") } - has_nrow <- !is.null(nrow_arg) && !is_missing(nrow_arg) - has_ncol <- !is.null(ncol_arg) && !is_missing(ncol_arg) - - if (is.null(x_arg) || is_missing(x_arg)) { - if (!has_nrow && !has_ncol) { - stop("argument \"nrow\" is missing, with no default", call. = FALSE) - } - if (!has_nrow && has_ncol) { + if (is.null(x_arg)) { + if (!has_nrow) { stop("argument \"nrow\" is missing, with no default", call. = FALSE) } x_val <- Fortran("1.0_c_double", Variable("double")) @@ -765,8 +713,28 @@ register_r2f_handler( "diag() only supports scalar, vector, or matrix inputs" ) - if (!has_nrow && !has_ncol && x_rank == 0L) { - nrow <- r2size(x_arg, scope) + # R's identity form is `length(x) == 1L` with no nrow/ncol -- it does + # not require a rank-0 value, so a declared `integer(1)` argument or a + # length-1 vector takes it too (R: diag(c(3)) is the 3x3 identity). + # The size comes from x's *value*, and the result is always double. + if (!has_nrow && !has_ncol && passes_as_scalar(x@value)) { + # R sizes the identity with as.integer(x), so a double or logical `x` + # is fine and truncates toward zero. Coerce in the size expression + # rather than requiring an integer, so diag(n) works whatever the + # caller declared. An integer `x` needs no wrapper. + size_arg <- if (identical(x@value@mode, "integer")) { + x_arg + } else if (x@value@mode %in% c("double", "logical")) { + call("as.integer", x_arg) + } else { + stop( + "diag(x) with a length-1 `x` builds an identity matrix of size ", + "`x`, which requires a numeric `x`; got ", + x@value@mode, + call. = FALSE + ) + } + nrow <- r2size(size_arg, scope) ncol <- nrow x_val <- Fortran("1.0_c_double", Variable("double")) return(diag_matrix( @@ -883,6 +851,9 @@ register_r2f_handler( ) # Shared crossprod/tcrossprod logic for one- and two-argument forms. +# `trans` says which side of the product is transposed: "T" for +# crossprod (t(x) %*% y), "N" for tcrossprod (x %*% t(y)); it fully +# determines the syrk form and both gemm op flags. crossprod_like <- function( x_arg, y_arg, @@ -890,17 +861,18 @@ crossprod_like <- function( ..., hoist, dest, - trans_single, - opA, - opB, + trans, context ) { + opA <- trans + opB <- if (identical(trans, "T")) "N" else "T" + x <- r2f(x_arg, scope, ..., hoist = hoist) - x <- maybe_cast_double(x) + x <- cast_linalg_double(x, context) if (is.null(y_arg)) { return(syrk( - trans = trans_single, + trans = trans, X = x, scope = scope, hoist = hoist, @@ -909,25 +881,25 @@ crossprod_like <- function( )) } - y <- maybe_cast_double(r2f(y_arg, scope, ..., hoist = hoist)) + y <- cast_linalg_double(r2f(y_arg, scope, ..., hoist = hoist), context) x_dims <- matrix_dims(x) y_dims <- matrix_dims(y) x_eff <- effective_dims(x_dims, opA) y_eff <- effective_dims(y_dims, opB) - conform <- check_conformable(x_eff$cols, y_eff$rows) - if (!conform$ok) { - stop("non-conformable arguments in ", context, call. = FALSE) - } - if (conform$unknown) { - stop( - "cannot verify conformability in ", - context, - " at compile time", - call. = FALSE - ) - } + guard_conformable_dims( + x_eff$cols, + y_eff$rows, + paste0("non-conformable arguments in ", context), + hoist, + scope, + left = x, + right = y, + left_axis = if (opA == "N") 2L else 1L, + right_axis = if (opB == "N") 1L else 2L, + checker = check_blas_dims + ) m <- x_eff$rows n <- y_eff$cols diff --git a/R/r2f-operators-helpers.R b/R/r2f-operators-helpers.R index 307ae98..e105388 100644 --- a/R/r2f-operators-helpers.R +++ b/R/r2f-operators-helpers.R @@ -29,8 +29,7 @@ booleanize_logical_as_int <- function(x) { # The only place cast spellings live; errors on casts it cannot spell # (complex/character operands, or any narrowing) so an unsupported mode is # a clean diagnostic instead of invalid generated Fortran. -# Used by: r2f-arithmetic.R, r2f-logical.R, r2f-constructors.R, -# r2f-reductions.R, r2f-matrix.R +# Used by: r2f-arithmetic.R, r2f-math.R, r2f-reductions.R, r2f-subscript.R cast_to_mode <- function(x, mode, context = "operand") { stopifnot(inherits(x, Fortran)) if ( @@ -85,11 +84,29 @@ maybe_cast_double <- function(x) { } } +# Cast a linear-algebra operand to double for the real BLAS/LAPACK +# lowerings (dgemm, dgesv, ...). Complex operands are refused: the d* +# routines would read complex storage as reals and return a plausible +# wrong answer, and quickr has no z* lowerings. R supports complex +# linear algebra, so the message names the divergence. +# Used by: r2f-matrix.R, r2f-matrix-parse.R, r2f-matrix-blas.R +cast_linalg_double <- function(x, context) { + if (identical(x@value@mode, "complex")) { + stop( + context, + " does not support complex operands; ", + "linear algebra in quickr is double-only", + call. = FALSE + ) + } + maybe_cast_double(x) +} + # Promote a list of operands to their common (lattice-join) mode, casting # each one whose mode differs. For contexts where Fortran requires uniform # argument types: array constructors (c()), min/max, merge, modulo. # Returns list(args = , mode = ). -# Used by: r2f-arithmetic.R, r2f-constructors.R, r2f-reductions.R +# Used by: r2f-conditionals.R, r2f-constructors.R promote_operands <- function(args, context = "operator") { mode <- reduce_promoted_mode(args) list( @@ -122,14 +139,109 @@ promote_arith_pair <- function(left, right, context = "arithmetic") { list(left = left, right = right) } -# Check if a dimension expression equals 1. +# Match `matrix(, nrow, ncol)`: a matrix() call +# matrix_call_args() accepts (data/nrow/ncol present, no +# byrow/dimnames) whose data is a length-1 literal or a declared +# scalar. Returns the matched arguments or NULL. Used by +# lower_elementwise_operands() to lower the fill to a native scalar +# broadcast instead of the O(nrow * ncol) temporary the matrix() +# handler would otherwise materialize; anything it declines falls back +# to the matrix() handler, which raises the real diagnostics. +match_scalar_matrix_fill <- function(e, scope) { + if (!is.call(e) || !identical(e[[1L]], quote(matrix))) { + return(NULL) + } + mc <- tryCatch(match.call(matrix, e), error = function(...) NULL) + if (is.null(mc)) { + return(NULL) + } + margs <- tryCatch( + matrix_call_args(as.list(mc)[-1L]), + error = function(...) NULL + ) + if (is.null(margs)) { + return(NULL) + } + data <- margs$data + data_is_scalar <- (is.atomic(data) && length(data) == 1L && !is.na(data)) || + (is.symbol(data) && + { + var <- get0(as.character(data), scope) + inherits(var, Variable) && passes_as_scalar(var) + }) + if (!data_is_scalar) { + return(NULL) + } + margs +} + +# Compile the two operands of an elementwise binary op. The one special +# case: `matrix(scalar, m, n)` against a genuine rank-2 array broadcasts +# natively -- compile just the scalar and enforce the claimed dims against +# the other operand (compile error when statically wrong, runtime guard +# when symbolic, spelled from the dim expressions since the fill has no +# array to size()). Everything else compiles as written. # Used by: r2f-arithmetic.R, r2f-logical.R +lower_elementwise_operands <- function(args, scope, ..., hoist = NULL) { + fills <- lapply(args, match_scalar_matrix_fill, scope = scope) + fill_idx <- which(!map_lgl(fills, is.null)) + + if (length(fill_idx) == 1L && !is.null(hoist)) { + j <- fill_idx + other <- r2f(args[[3L - j]], scope, ..., hoist = hoist) + fill_dims <- r2dims(list(fills[[j]]$nrow, fills[[j]]$ncol), scope) + fill_dims_f <- map_chr(fill_dims, \(d) dims2f(list(d), scope)) + broadcastable <- inherits(other, Fortran) && + !is.null(other@value) && + other@value@rank == 2L && + !passes_as_scalar(other@value) && + !any(map_lgl(fill_dims, is_scalar_na)) && + all(nzchar(fill_dims_f)) && + !any(grepl(":", fill_dims_f, fixed = TRUE)) + if (broadcastable) { + other_dims <- matrix_dims(other) + for (axis in 1:2) { + # The fill has no array to size(), so its side of a runtime guard + # is spelled from the claimed dim expression via `left_f`. + guard_conformable_dims( + fill_dims[[axis]], + if (axis == 1L) other_dims$rows else other_dims$cols, + elementwise_matrix_msg, + hoist, + scope, + left = NULL, + right = other, + right_axis = axis, + left_f = glue("({fill_dims_f[[axis]]})") + ) + } + fill <- r2f(fills[[j]]$data, scope, ..., hoist = hoist) + out <- list(fill, other) + return(if (j == 1L) out else rev(out)) + } + fallback <- r2f(args[[j]], scope, ..., hoist = hoist) + out <- list(fallback, other) + return(if (j == 1L) out else rev(out)) + } + + lapply(args, r2f, scope, ..., hoist = hoist) +} + +# Check if a dimension expression equals 1. +# Used by: r2f-matrix.R, is_one_by_one() dim_is_one <- function(x) { is_wholenumber(x) && identical(as.integer(x), 1L) } +# Check if a dimension expression is statically known and not 1. Symbolic +# dimensions are FALSE: "not provably 1" is not "provably not 1". +# Used by: conform_elementwise_operands() +dim_known_not_one <- function(x) { + is_wholenumber(x) && !identical(as.integer(x), 1L) +} + # Check if a Fortran value is a 1x1 matrix. -# Used by: r2f-arithmetic.R, r2f-logical.R +# Used by: conform_elementwise_operands() is_one_by_one <- function(x) { stopifnot(inherits(x, Fortran)) x@value@rank == 2L && @@ -137,8 +249,12 @@ is_one_by_one <- function(x) { dim_is_one(x@value@dims[[2L]]) } -# Check if two dimension expressions match. -# Used by: r2f-arithmetic.R, r2f-logical.R +# Check if two dimension expressions provably match (both known and +# equal, or the identical symbolic expression). Weaker than +# check_elementwise_lengths(): no zero-length policy, no symbol +# normalization -- callers use it for routing/declaration decisions, not +# for the conformability contract. +# Used by: r2f-matrix.R (bind_common_dim) dims_match <- function(left, right) { if (is_wholenumber(left) && is_wholenumber(right)) { return(identical(as.integer(left), as.integer(right))) @@ -146,29 +262,126 @@ dims_match <- function(left, right) { identical(left, right) } -# Check if two lengths recycle without warnings. -# Used by: r2f-arithmetic.R, r2f-logical.R -check_recyclable_pair <- function(left, right) { +# Check if two dimension expressions are *proven* equal: both known and +# equal, or structurally identical after symbol normalization. NA dims +# are never proven (two unknown lengths are not the same quantity). +# Stronger than dims_match() (normalizes symbols), stricter than +# check_elementwise_lengths() (an incomparable pair is FALSE, not a +# deferred runtime guard). +# Used by: r2f-matrix-blas.R (dest_dims_proven_equal) +dims_proven_equal <- function(left, right) { + if (is_scalar_na(left) || is_scalar_na(right)) { + return(FALSE) + } + if (is_wholenumber(left) && is_wholenumber(right)) { + return(identical(as.integer(left), as.integer(right))) + } + identical(fortranize_expr_symbols(left), fortranize_expr_symbols(right)) +} + +# Three-valued conformability verdict for one axis of an elementwise op: +# ok+known (no guard needed), not-ok+known (compile error at the caller), +# or unknown (caller emits a runtime guard). Known lengths must be equal +# and nonzero -- R-style recycling is never implemented, and quickr cannot +# represent length-0 results. NA dims are always unknown: two unknown +# lengths are not the same quantity. +# Used by: guard_conformable_dims() +check_elementwise_lengths <- function(left, right) { if (is_wholenumber(left) && is_wholenumber(right)) { left <- as.integer(left) right <- as.integer(right) - if (left == 0L || right == 0L) { + return(list(ok = left == right && left > 0L, unknown = FALSE)) + } + if ( + (is_wholenumber(left) && as.integer(left) == 0L) || + (is_wholenumber(right) && as.integer(right) == 0L) + ) { + return(list(ok = FALSE, unknown = FALSE)) + } + if (!is_scalar_na(left) && !is_scalar_na(right)) { + left_norm <- fortranize_expr_symbols(left) + right_norm <- fortranize_expr_symbols(right) + if (identical(left_norm, right_norm)) { return(list(ok = TRUE, unknown = FALSE)) } - longer <- max(left, right) - shorter <- min(left, right) - return(list(ok = (longer %% shorter) == 0L, unknown = FALSE)) - } - left_norm <- fortranize_expr_symbols(left) - right_norm <- fortranize_expr_symbols(right) - if (identical(left_norm, right_norm)) { - return(list(ok = TRUE, unknown = FALSE)) } list(ok = TRUE, unknown = TRUE) } +# The message shared by every enforcement point of the elementwise matrix +# shape contract: the runtime-guard text must match the compile-error text. +elementwise_matrix_msg <- + "elementwise matrix operations require matching dimensions" + +# Render one side of a dim-comparison guard: a caller-provided spelling +# (`f`, for operands with no array to size(), e.g. a scalar fill's claimed +# dims) wins; then a literal dim as the literal; anything else as the +# operand's actual extent (whole size when `axis` is NULL). size() is an +# inquiry, so applying it to operand expression text does not evaluate the +# operand. +# Used by: guard_conformable_dims() +dimension_guard_expr <- function(dim, operand, axis = NULL, f = NULL) { + if (!is.null(f)) { + return(f) + } + if (is_wholenumber(dim)) { + return(as.character(as.integer(dim))) + } + if (is.null(axis)) { + glue("size({operand}, kind=c_ptrdiff_t)") + } else { + glue("size({operand}, {axis}, kind=c_ptrdiff_t)") + } +} + +# Shared conformability guard emitter. `checker` supplies the caller's +# static policy: elementwise operations require equal nonzero dimensions, +# while BLAS/LAPACK callers use equality semantics that permit zero. +# A statically known mismatch is a compile error; unknown dimensions get a +# statement-level runtime guard; provably equal dimensions need nothing. +# `axis` NULL compares the operand's whole size (rank-1 operands). +# +# `hoist` is always live: r2f() opens one per statement before dispatching +# to a handler, and every caller forwards the one it received. +# emit_quickr_error_if() asserts it. +# Used by: conform_elementwise_operands(), lower_elementwise_operands(), +# r2f-conditionals.R, r2f-matrix*.R. `left_f`/`right_f` override that +# side's guard spelling (see dimension_guard_expr()); its `left`/`right` operand is +# then unused and may be NULL. +guard_conformable_dims <- function( + left_dim, + right_dim, + message, + hoist, + scope, + left, + right, + left_axis = NULL, + right_axis = NULL, + left_f = NULL, + right_f = NULL, + checker = check_elementwise_lengths +) { + stopifnot(is_string(message), is.function(checker)) + conform <- checker(left_dim, right_dim) + if (!conform$ok) { + stop(message, call. = FALSE) + } + if (conform$unknown) { + emit_quickr_error_if( + glue( + "{dimension_guard_expr(left_dim, left, left_axis, left_f)} /= {dimension_guard_expr(right_dim, right, right_axis, right_f)}" + ), + message, + hoist, + scope + ) + } + invisible(TRUE) +} + # Reshape a vector to match a matrix's dimensions. -# Used by: r2f-arithmetic.R, r2f-logical.R +# Used by: r2f-constructors.R, conform_elementwise_operands() reshape_vector_for_matrix <- function(vec, rows, cols) { stopifnot(inherits(vec, Fortran)) out_val <- Variable(vec@value@mode, list(rows, cols)) @@ -183,17 +396,73 @@ reshape_vector_for_matrix <- function(vec, rows, cols) { Fortran(out_expr, out_val) } +# Size expressions may carry an as.integer() coercion (diag()'s identity +# form sizes the result with as.integer(x), as R does). The two renderers +# that spell a dim by deparsing -- bind_dim_string() and blas_int() -- would +# emit the R name verbatim, so map it to Fortran's INT(), which truncates +# toward zero the same way. dims2f()/dims2c() translate the call properly +# and do not need this. +# Used by: bind_dim_string() (r2f-matrix.R), blas_int() (r2f-matrix-blas.R) +fortranize_size_calls <- function(e) { + if (!is.call(e)) { + return(e) + } + if (identical(e[[1L]], quote(as.integer))) { + e[[1L]] <- quote(int) + } + for (i in seq_along(e)[-1L]) { + if (!is_missing(e[[i]])) { + e[[i]] <- fortranize_size_calls(e[[i]]) + } + } + e +} + +# Floor a double expression while staying in the real domain: Fortran +# FLOOR() returns an integer, so a large double (e.g. 1e20) would +# silently overflow. aint(x) truncates toward 0 (real result); adjust by +# -1 where truncation differs from floor (negative non-integers). `x` is +# spliced three times, so callers hoist non-trivial expressions first. +# Used by: r2f-math.R (floor), r2f-arithmetic.R (double %/%) +real_floor_expr <- function(x) { + aint <- glue("aint({x})") + glue("({aint} - merge(1.0_c_double, 0.0_c_double, ({x} < {aint})))") +} + # Convert a 1x1 matrix to a scalar. -# Used by: r2f-arithmetic.R, r2f-logical.R +# Used by: r2f-matrix.R, conform_elementwise_operands() scalarize_matrix <- function(mat) { stopifnot(inherits(mat, Fortran)) out_val <- Variable(mat@value@mode) Fortran(glue("{mat}(1, 1)"), out_val) } -# Reshape vector/matrix operands to match ranks for binary operations. +# Reshape vector/matrix operands to match ranks for binary operations, and +# enforce the elementwise conformability policy via guard_conformable_dims(): +# known-mismatched lengths are compile errors (R-style recycling is not +# supported), while unknown lengths get a runtime guard. Scalar broadcast +# requires a value represented as scalar at translation time, such as +# `double(1)`; an assumed-shape `double(NA)` remains a vector even when its +# runtime length is one. +# +# `scalarize_one_by_one` mirrors R's split over length-1 arrays: arithmetic +# recycles a 1x1 matrix against a vector of statically known length != 1 +# (deprecated in R but still the behavior: R drops the array dims). When +# the vector's length is only known at run time, the result's shape would +# depend on that value -- R keeps the 1x1 dims for a length-1 vector and +# drops them otherwise -- so the 1x1 falls through to the vector-matrix +# rule: a runtime guard requires length 1 and the result is a 1x1 matrix, +# an error where R would recycle. Comparisons and & | error in R itself, +# so strict callers pass FALSE and the 1x1 always takes the vector-matrix +# path. # Used by: r2f-arithmetic.R, r2f-logical.R -maybe_reshape_vector_matrix <- function(left, right) { +conform_elementwise_operands <- function( + left, + right, + hoist, + scope, + scalarize_one_by_one = TRUE +) { if ( !inherits(left, Fortran) || !inherits(right, Fortran) || @@ -208,67 +477,103 @@ maybe_reshape_vector_matrix <- function(left, right) { left_rank <- if (left_scalar) 0L else left@value@rank right_rank <- if (right_scalar) 0L else right@value@rank - if (left_rank == 2L && right_rank == 1L && is_one_by_one(left)) { + # Casts and booleanization wrap operands in expression text that Fortran + # cannot index (`real(x, kind=c_double)(1, 1)` is invalid), so hoist + # anything that is not a bare name before subscripting it. + scalarize_via_hoist <- function(x) { + if (!is.null(hoist)) { + x <- hoist_unless_name(x, hoist) + } + scalarize_matrix(x) + } + + if ( + scalarize_one_by_one && + left_rank == 2L && + right_rank == 1L && + is_one_by_one(left) + ) { right_len <- dim_or_one(right, 1L) - if (!dim_is_one(right_len)) { - left <- scalarize_matrix(left) + if (dim_known_not_one(right_len)) { + left <- scalarize_via_hoist(left) left_rank <- 0L } - } else if (left_rank == 1L && right_rank == 2L && is_one_by_one(right)) { + } else if ( + scalarize_one_by_one && + left_rank == 1L && + right_rank == 2L && + is_one_by_one(right) + ) { left_len <- dim_or_one(left, 1L) - if (!dim_is_one(left_len)) { - right <- scalarize_matrix(right) + if (dim_known_not_one(left_len)) { + right <- scalarize_via_hoist(right) right_rank <- 0L } } if (left_rank == 1L && right_rank == 1L) { - conform <- check_recyclable_pair( + vector_msg <- paste0( + "elementwise vector operations require equal lengths or ", + "a scalar operand; R-style recycling is not supported" + ) + guard_conformable_dims( dim_or_one(left, 1L), - dim_or_one(right, 1L) + dim_or_one(right, 1L), + vector_msg, + hoist, + scope, + left = left, + right = right ) - if (!conform$ok) { - stop( - "elementwise vector operations require lengths that recycle cleanly unless one operand is scalar", - call. = FALSE - ) - } } if (left_rank == 2L && right_rank == 2L) { left_dims <- matrix_dims(left) right_dims <- matrix_dims(right) - row_conform <- check_conformable(left_dims$rows, right_dims$rows) - col_conform <- check_conformable(left_dims$cols, right_dims$cols) - if (!row_conform$ok || !col_conform$ok) { - stop( - "elementwise matrix operations require matching dimensions", - call. = FALSE + for (axis in 1:2) { + guard_conformable_dims( + if (axis == 1L) left_dims$rows else left_dims$cols, + if (axis == 1L) right_dims$rows else right_dims$cols, + elementwise_matrix_msg, + hoist, + scope, + left = left, + right = right, + left_axis = axis, + right_axis = axis ) } } + vec_mat_msg <- paste0( + "elementwise vector-matrix operations require a scalar or ", + "a vector length equal to the matrix first dimension (nrow)" + ) if (left_rank == 1L && right_rank == 2L) { right_dims <- matrix_dims(right) - left_len <- dim_or_one(left, 1L) - row_conform <- check_conformable(left_len, right_dims$rows) - if (!row_conform$ok || row_conform$unknown) { - stop( - "elementwise vector-matrix operations require a scalar or a vector length equal to the matrix first dimension (nrow)", - call. = FALSE - ) - } + guard_conformable_dims( + dim_or_one(left, 1L), + right_dims$rows, + vec_mat_msg, + hoist, + scope, + left = left, + right = right, + right_axis = 1L + ) left <- reshape_vector_for_matrix(left, right_dims$rows, right_dims$cols) } else if (left_rank == 2L && right_rank == 1L) { left_dims <- matrix_dims(left) - right_len <- dim_or_one(right, 1L) - row_conform <- check_conformable(right_len, left_dims$rows) - if (!row_conform$ok || row_conform$unknown) { - stop( - "elementwise vector-matrix operations require a scalar or a vector length equal to the matrix first dimension (nrow)", - call. = FALSE - ) - } + guard_conformable_dims( + dim_or_one(right, 1L), + left_dims$rows, + vec_mat_msg, + hoist, + scope, + left = right, + right = left, + right_axis = 1L + ) right <- reshape_vector_for_matrix(right, left_dims$rows, left_dims$cols) } @@ -281,13 +586,225 @@ maybe_reshape_vector_matrix <- function(left, right) { mode_lattice <- c("logical", "integer", "double", "complex") # Rank of a mode on the lattice (NA for modes outside it, e.g. character). -# Used by: reduce_promoted_mode(), scope.R (check_reassignment_narrowing) +# Used by: reduce_promoted_mode(), check_reassignment_narrowing() mode_rank <- function(mode) { match(mode, mode_lattice) } +# A dim expression can be spelled in a runtime guard only if every +# self-size symbol it references (`foo__len_`, `foo__dim_1_`) belongs +# to an external variable -- those arrive as size dummies; a local's +# self-sizes are phantoms backing implicit allocation and do not exist +# in the generated Fortran. +# Used by: check_assignment_compatible() +dim_guard_spellable <- function(dim, scope) { + if (!is.language(dim)) { + return(TRUE) + } + if (is.null(scope)) { + return(FALSE) + } + matches <- regmatches( + syms <- all.vars(dim), + regexec("^(.*)__(dim_[0-9]+|len)_$", syms) + ) + all(vapply( + matches, + function(match) { + if (!length(match)) { + return(TRUE) # not a self-size symbol + } + var <- get0(match[[2L]], scope) + inherits(var, Variable) && var@is_external + }, + logical(1) + )) +} + +# Reassignment cannot re-declare a Fortran variable to a new shape the +# way R rebinds a symbol, so rank and every extent must stay +# compatible. Scalars broadcast natively into an array target (an +# existing divergence: R rebinds the symbol to the scalar), and a +# deferred-shape local (declared with NA dims) reallocates extents on +# same-rank whole-array assignment, matching R, so it is exempt. Per +# axis, the conformability policy applies: a statically known mismatch +# is a compile error, dims that cannot be compared statically get a +# statement-level runtime guard through `hoist` (spelled from the dim +# expressions, when spellable), and provably equal dims need nothing. +# Callers with no statement context (no hoist) get the static checks +# only. +# Used by: r2f-assign.R, scope.R +check_assignment_compatible <- function( + name, + target, + value, + hoist = NULL, + scope = NULL +) { + if ( + is.null(value) || + !inherits(target, Variable) || + !inherits(value, Variable) + ) { + return(invisible()) + } + target_scalar <- passes_as_scalar(target) + value_scalar <- passes_as_scalar(value) + # Two length-1 values conform whatever their ranks (a declared `double(1)` + # is rank 1; a literal is rank 0). + if (target_scalar && value_scalar) { + return(invisible()) + } + deferred_local <- !target@is_external && has_self_size_dims(target) + if (target_scalar || value_scalar) { + # A deferred-shape local can genuinely take a new array shape. + if (deferred_local && !value_scalar) { + return(invisible()) + } + # Otherwise one side is length 1 and the other is a real array. R + # rebinds the symbol to the new shape; Fortran cannot, and would + # silently broadcast a scalar across the array (or drop all but the + # first element of an array into a scalar). + stop( + "cannot reassign `", + name, + "`: replacement is ", + if (value_scalar) "a scalar" else "an array", + " but `", + name, + "` is ", + if (target_scalar) "a scalar" else "an array", + "; R would rebind `", + name, + "` to the new shape", + call. = FALSE + ) + } + if (target@rank != value@rank) { + stop( + "cannot reassign `", + name, + "`: replacement rank (", + value@rank, + ") differs from the declared rank (", + target@rank, + "); R would rebind `", + name, + "` to the new shape", + call. = FALSE + ) + } + if (deferred_local) { + # deferred-shape local: implicit (re)allocation matches R's rebind + return(invisible()) + } + emitted <- character() + for (axis in seq_len(target@rank)) { + t_dim <- target@dims[[axis]] + v_dim <- value@dims[[axis]] + if (is_scalar_na(t_dim) || is_scalar_na(v_dim)) { + next + } + if (is_wholenumber(t_dim) && is_wholenumber(v_dim)) { + if (!identical(as.integer(t_dim), as.integer(v_dim))) { + stop( + "cannot reassign `", + name, + "`: dimension ", + axis, + " would change from ", + as.integer(t_dim), + " to ", + as.integer(v_dim), + "; R would rebind `", + name, + "` to the new shape", + call. = FALSE + ) + } + next + } + if ( + identical( + fortranize_expr_symbols(t_dim), + fortranize_expr_symbols(v_dim) + ) + ) { + next + } + if (is.null(hoist)) { + next + } + if ( + dim_guard_spellable(t_dim, scope) && dim_guard_spellable(v_dim, scope) + ) { + condition <- glue( + "({dims2f(list(t_dim), scope)}) /= ({dims2f(list(v_dim), scope)})" + ) + } else if (!is.null(target@name) && !is.null(value@name)) { + condition <- glue( + "size({target@name}, {axis}, kind=c_ptrdiff_t) /= size({value@name}, {axis}, kind=c_ptrdiff_t)" + ) + } else { + stop( + "cannot reassign `", + name, + "`: runtime shape cannot be validated", + call. = FALSE + ) + } + # Different axes can spell the same guard (e.g. a square dest vs a + # square result, m/=k on both axes); emit each condition once. + if (condition %in% emitted) { + next + } + emitted <- c(emitted, condition) + emit_quickr_error_if( + condition, + sprintf("reassignment must preserve the shape of `%s`", name), + hoist, + scope + ) + } + invisible() +} + +# Reassignment cannot re-type a Fortran variable the way R promotes an R +# binding, so a value whose mode sits above the variable's on the lattice +# would be silently truncated by the assignment. Refuse at compile time +# instead. +# Used by: r2f-assign.R +check_reassignment_narrowing <- function(name, target, value) { + if ( + !inherits(target, Variable) || + !inherits(value, Variable) || + is.null(target@mode) || + is.null(value@mode) + ) { + return() + } + target_rank <- mode_rank(target@mode) + value_rank <- mode_rank(value@mode) + if (is.na(target_rank) || is.na(value_rank) || value_rank <= target_rank) { + return() + } + stop( + "cannot reassign `", + name, + "`: assignment would narrow ", + value@mode, + " to ", + target@mode, + "; R would promote `", + name, + "` to ", + value@mode, + call. = FALSE + ) +} + # Determine the promoted mode from a list of Fortran values. -# Used by: r2f-arithmetic.R, r2f-constructors.R +# Used by: r2f-arithmetic.R, r2f-matrix.R reduce_promoted_mode <- function(...) { getmode <- function(d) { if (inherits(d, Fortran)) { @@ -311,8 +828,8 @@ reduce_promoted_mode <- function(...) { } # Create a Variable with conforming dimensions from multiple inputs. -# Used by: r2f-arithmetic.R, r2f-logical.R, r2f-constructors.R, r2f-conditionals.R -conform <- function(..., mode = NULL) { +# Used by: r2f-arithmetic.R, r2f-logical.R +infer_result_variable <- function(..., mode = NULL) { vars <- drop_nulls(list(...)) # Report the promoted (lattice-join) mode: the emitted expression already # promotes (Fortran's rules match R for numeric mixes), and `<-` copies diff --git a/R/r2f-random.R b/R/r2f-random.R index a3aa27a..a798fe7 100644 --- a/R/r2f-random.R +++ b/R/r2f-random.R @@ -17,9 +17,9 @@ r2f_handlers[["runif"]] <- function(args, scope, ..., hoist = NULL) { # R evaluates runif() bounds exactly once, but `min` is spliced twice below # and the implied-do re-evaluates the whole expression per element; hoist # non-trivial bounds (e.g. an impure runif(1)) so they are evaluated once. + # (hoist_unless_name() leaves names and literals alone.) bound <- function(r_arg) { - b <- r2f(r_arg, scope, ..., hoist = hoist) - if (is.atomic(r_arg)) b else hoist_unless_name(b, hoist) + hoist_unless_name(r2f(r_arg, scope, ..., hoist = hoist), hoist) } if (default_min && default_max) { diff --git a/R/r2f-reductions-helpers.R b/R/r2f-reductions-helpers.R index e0d602f..0c08269 100644 --- a/R/r2f-reductions-helpers.R +++ b/R/r2f-reductions-helpers.R @@ -30,6 +30,31 @@ create_mask_hoist <- function() { environment() } +# Lower one reduction argument under a fresh mask hoister. Nested +# reductions (e.g. sum(x * as.double(any(x[m] > 1)))) can thread an +# existing hoist_mask through `...`; each reduction context installs +# exactly one mask hoister, so the inherited one is dropped rather than +# forwarded -- forwarding both would hand the `[` handler two hoist_mask +# arguments. A conflicting second mask within the same context is a +# clean compile error. +# Used by: r2f-reductions.R (max/min/sum/prod and any/all) +lower_masked_reduction_arg <- function(arg, scope, mask_hoist, dots) { + x <- r2f( + arg, + scope, + calls = dots$calls, + hoist = dots$hoist, + hoist_mask = mask_hoist$try_set + ) + if (mask_hoist$has_conflict()) { + stop( + "reduction expressions only support a single logical mask", + call. = FALSE + ) + } + x +} + # Convert a linear 1D index to multi-dimensional subscripts. # Used by: r2f-reductions.R, r2f-subscript.R, r2f-control-flow.R linear_subscripts_from_1d <- function(base_name, rank, idx) { diff --git a/R/r2f-reductions.R b/R/r2f-reductions.R index 8cc2c74..af5a09c 100644 --- a/R/r2f-reductions.R +++ b/R/r2f-reductions.R @@ -4,6 +4,23 @@ # - logical: any, all # - index: which.max, which.min +# --- Helpers --- + +# A c(TRUE)-style literal lowers to a rank-1 Fortran array constructor +# ("[ ... ]") even when its value passes as a scalar; any()/all() must +# wrap such values to reduce them back to a scalar expression. +# Used by: any/all handler +renders_as_array_constructor <- function(f) { + startsWith(trimws(as.character(f)), "[") +} + +# TRUE for values declared length-1 (rank-1, dims list(1L)): scalar in +# the ABI, but not a Fortran scalar expression. +# Used by: any/all handler +is_declared_len1 <- function(f) { + !is.null(f@value) && identical(f@value@dims, list(1L)) +} + # --- Handlers --- register_r2f_handler( @@ -23,8 +40,9 @@ register_r2f_handler( ) } + call_name <- last(list(...)$calls) intrinsic <- switch( - last(list(...)$calls), + call_name, max = "maxval", min = "minval", sum = "sum", @@ -33,30 +51,10 @@ register_r2f_handler( reduce_arg <- function(arg) { mask_hoist <- create_mask_hoist() - # Nested reductions (e.g., min(max(...), ...)) can thread an existing - # hoist_mask through `...`. We always want a single mask hoister per - # reduction context, so we ignore any inherited one and install ours. - dots <- list(...) - x <- r2f( - arg, - scope, - calls = dots$calls, - hoist = dots$hoist, - hoist_mask = mask_hoist$try_set - ) - if (mask_hoist$has_conflict()) { - stop( - "reduction expressions only support a single logical mask", - call. = FALSE - ) - } + x <- lower_masked_reduction_arg(arg, scope, mask_hoist, list(...)) # R's numeric reductions treat logicals as integers (sum(TRUE) is 1L), # and Fortran's sum/product/minval/maxval reject logical arrays. - x <- cast_to_mode( - x, - arith_join_mode(x), - sprintf("%s()", last(dots$calls)) - ) + x <- cast_to_mode(x, arith_join_mode(x), sprintf("%s()", call_name)) if (x@value@is_scalar) { return(x) } @@ -80,10 +78,14 @@ register_r2f_handler( # don't strictly need it, but one code path beats two. Logical # operands join as integer (R: max(TRUE, FALSE) is 1L). mode <- arith_join_mode(args) - context <- sprintf("%s()", last(list(...)$calls)) - args <- lapply(args, cast_to_mode, mode = mode, context = context) + args <- lapply( + args, + cast_to_mode, + mode = mode, + context = sprintf("%s()", call_name) + ) s <- switch( - last(list(...)$calls), + call_name, max = glue("max({str_flatten_commas(args)})"), min = glue("min({str_flatten_commas(args)})"), sum = glue("({str_flatten(args, ' + ')})"), @@ -127,13 +129,7 @@ register_r2f_handler( reduce_arg <- function(arg) { mask_hoist <- create_mask_hoist() - x <- r2f(arg, scope, ..., hoist_mask = mask_hoist$try_set) - if (mask_hoist$has_conflict()) { - stop( - "reduction expressions only support a single logical mask", - call. = FALSE - ) - } + x <- lower_masked_reduction_arg(arg, scope, mask_hoist, list(...)) if (!identical(x@value@mode, "logical")) { stop("any()/all() only implemented for logical", call. = FALSE) @@ -146,8 +142,7 @@ register_r2f_handler( if (is.null(hoisted_mask)) { # `c(FALSE)` lowers to a 1-element Fortran array constructor # (`[.false.]`) but any()/all() must still return scalars. - x_code <- trimws(as.character(x)) - if (startsWith(x_code, "[")) { + if (renders_as_array_constructor(x)) { return(Fortran(glue("{intrinsic}({x})"), Variable("logical"))) } return(x) @@ -161,16 +156,12 @@ register_r2f_handler( # # Conversely, literal masks like `c(FALSE)` compile to array constructors # (e.g. `[ .false. ]`) and must be reduced to a scalar condition. - mask_code <- trimws(as.character(hoisted_mask)) - is_array_ctor <- startsWith(mask_code, "[") mask_is_scalar <- !is.null(hoisted_mask@value) && passes_as_scalar(hoisted_mask@value) && - !is_array_ctor + !renders_as_array_constructor(hoisted_mask) - mask_len1 <- - !is.null(hoisted_mask@value) && - identical(hoisted_mask@value@dims, list(1L)) + mask_len1 <- is_declared_len1(hoisted_mask) if (!mask_is_scalar && !mask_len1) { stop( @@ -190,8 +181,7 @@ register_r2f_handler( # - any(logical(0)) == FALSE # - all(logical(0)) == TRUE identity <- if (identical(call_name, "any")) ".false." else ".true." - x_code <- trimws(as.character(x)) - x_scalar <- if (startsWith(x_code, "[")) { + x_scalar <- if (renders_as_array_constructor(x)) { glue("{intrinsic}({x})") } else { glue("{x}") @@ -213,12 +203,9 @@ register_r2f_handler( # Note: A length-1 mask constructor like `c(TRUE)` compiles to a rank-1 # array constructor (`[ .true. ]`). In R, this is recycled as a scalar # mask, so we must scalarize it to keep elementwise ops conformable. - mask_code <- trimws(as.character(hoisted_mask)) - mask_is_array_ctor <- startsWith(mask_code, "[") mask_ctor_len1 <- - mask_is_array_ctor && - !is.null(hoisted_mask@value) && - identical(hoisted_mask@value@dims, list(1L)) + renders_as_array_constructor(hoisted_mask) && + is_declared_len1(hoisted_mask) mask_expr <- if (mask_ctor_len1) { glue("any({hoisted_mask})") } else { @@ -248,6 +235,7 @@ register_r2f_handler( r2f_handlers[["which.max"]] <- r2f_handlers[["which.min"]] <- function(args, scope = NULL, ...) { stopifnot(length(args) == 1) + call_name <- last(list(...)$calls) x <- r2f(args[[1L]], scope, ...) stopifnot( "Values passed to which.max()/which.min() must be 1d arrays" = x@value@rank == @@ -273,8 +261,6 @@ r2f_handlers[["which.max"]] <- r2f_handlers[["which.min"]] <- # while retaining early-exit. # Results are compiler/runtime dependent; the relative pattern was stable. # - call_name <- last(list(...)$calls) - has_var_name <- inherits(x@value, Variable) && !is.null(x@value@name) use_lgl_storage <- has_var_name && !logical_as_int(x@value) int_backed_expr <- @@ -302,7 +288,7 @@ r2f_handlers[["which.max"]] <- r2f_handlers[["which.min"]] <- f <- glue("max(1_c_int, {loc})") } else { intrinsic <- switch( - last(list(...)$calls), + call_name, which.max = "maxloc", which.min = "minloc" ) diff --git a/R/r2f-rev.R b/R/r2f-rev.R index f1806ce..e69a8ef 100644 --- a/R/r2f-rev.R +++ b/R/r2f-rev.R @@ -30,13 +30,13 @@ r2f_handlers[["rev"]] <- function(args, scope, ..., hoist = NULL) { # Fortran array sections require an array designator; hoist array expressions. if (is.null(x@value@name)) { - tmp <- hoist$declare_tmp( + x <- materialize_via_hoist( + x, mode = x@value@mode, dims = x@value@dims, + hoist = hoist, logical_as_int = logical_as_int(x@value) ) - hoist$emit(glue("{tmp@name} = {x}")) - x <- Fortran(tmp@name, tmp) } base_name <- x@value@name %||% diff --git a/R/r2f-subscript.R b/R/r2f-subscript.R index 9f37721..ba37801 100644 --- a/R/r2f-subscript.R +++ b/R/r2f-subscript.R @@ -1,6 +1,63 @@ # r2f-subscript.R # Handlers for subscripting operations: [ +# --- Shared subscript lowering --- +# The read side (the `[` handler below) and the write side +# (compile_subset_designator() in r2f-closures.R) lower subscripts with +# the same helpers so the two paths cannot drift. + +# Fortran subscripts must be integers; coerce a double subscript +# expression with a c_ptrdiff_t cast. (Not cast_to_mode(), which refuses +# narrowing casts by design.) +cast_subscript_to_integer <- function(sub) { + if (sub@value@mode == "double") { + Fortran( + glue("int({sub}, kind=c_ptrdiff_t)"), + Variable("integer", sub@value@dims) + ) + } else { + sub + } +} + +# Lower raw subscript args: a missing arg becomes a full-axis `:` +# section, everything else compiles through r2f(). Passing the +# per-statement hoist along matters: subscript expressions that need +# temporaries (e.g. rev(seq_len(n))) would otherwise self-render as an +# inline `block ... end block` *expression*, which is invalid Fortran +# inside an array designator. +lower_subscript_args <- function(idx_args, base_dims, scope, ..., hoist) { + idxs <- whole_doubles_to_ints(idx_args) + imap(idxs, function(idx, i) { + if (is_missing(idx)) { + Fortran(":", Variable("integer", base_dims[[i]])) + } else { + cast_subscript_to_integer(r2f(idx, scope, ..., hoist = hoist)) + } + }) +} + +# Indexing a scalar (rank-1 length-1) with `[1]` (or the singleton loop +# index) is valid in R, but Fortran scalars cannot be subscripted; +# callers treat it as a no-op. +subscript_is_scalar_noop <- function(base_value, idxs) { + if ( + passes_as_scalar(base_value) && + length(idxs) == 1 && + idxs[[1]]@value@mode == "integer" && + passes_as_scalar(idxs[[1]]@value) + ) { + idx_r <- attr(idxs[[1]], "r", exact = TRUE) + if (identical(idx_r, 1L) || identical(idx_r, 1)) { + return(TRUE) + } + if (isTRUE(idxs[[1]]@value@loop_is_singleton)) { + return(TRUE) + } + } + FALSE +} + # --- Handlers --- r2f_handlers[["["]] <- function( @@ -10,11 +67,15 @@ r2f_handlers[["["]] <- function( hoist_mask = function(mask) FALSE, hoist = NULL ) { - # only a subset of R's x[...] features can be translated here. `...` can only be: - # - a single logical mask, of the same rank as `x`. returns a rank 1 vector. - # - a number of arguments matching the rank of `x`, with each being - # an integer of rank 0 or 1. In this case, a rank 1 logical becomes - # converted to an integer with + # Only a subset of R's x[...] features can be translated here. `...` + # can be: + # - a single logical mask of the same rank as `x`: lowers to pack(), + # returning a rank-1 vector. + # - a single scalar integer subscript on a rank>1 `x`: R-style linear + # indexing. + # - one subscript per axis of `x`, each logical or integer of rank 0 + # or 1 (a logical vector becomes integer positions, as R's which(); + # double subscripts coerce to integer). var <- args[[1]] var <- r2f(var, scope, ..., hoist = hoist) @@ -25,27 +86,13 @@ r2f_handlers[["["]] <- function( check_subscript_exprs(var@value, idx_args) - idxs <- whole_doubles_to_ints(idx_args) - idxs <- imap(idxs, function(idx, i) { - if (is_missing(idx)) { - Fortran(":", Variable("integer", var@value@dims[[i]])) - } else { - # Important: pass along the per-statement hoist context, otherwise - # subscript expressions that need temporaries (e.g. rev(seq_len(n))) - # will self-render as an inline `block ... end block` *expression*, - # which is invalid Fortran inside an array designator. - sub <- r2f(idx, scope, ..., hoist = hoist) - if (sub@value@mode == "double") { - # Fortran subscripts must be integers; coerce numeric expressions - Fortran( - glue("int({sub}, kind=c_ptrdiff_t)"), - Variable("integer", sub@value@dims) - ) - } else { - sub - } - } - }) + idxs <- lower_subscript_args( + idx_args, + var@value@dims, + scope, + ..., + hoist = hoist + ) if ( length(idxs) == 1 && @@ -63,21 +110,8 @@ r2f_handlers[["["]] <- function( )) } - # Indexing a scalar (rank-1 length-1) with `[1]` is valid in R, but Fortran - # scalars cannot be subscripted. Treat it as a no-op. - if ( - passes_as_scalar(var@value) && - length(idxs) == 1 && - idxs[[1]]@value@mode == "integer" && - passes_as_scalar(idxs[[1]]@value) - ) { - idx_r <- attr(idxs[[1]], "r", exact = TRUE) - if (identical(idx_r, 1L) || identical(idx_r, 1)) { - return(var) - } - if (isTRUE(idxs[[1]]@value@loop_is_singleton)) { - return(var) - } + if (subscript_is_scalar_noop(var@value, idxs)) { + return(var) } # R-style linear indexing for rank>1 arrays: x[i] @@ -89,9 +123,12 @@ r2f_handlers[["["]] <- function( ) { # Hoist array expressions before subscripting (no invalid (expr)(i)). if (!passes_as_scalar(var@value) && is.null(var@value@name)) { - tmp <- hoist$declare_tmp(mode = var@value@mode, dims = var@value@dims) - hoist$emit(glue("{tmp@name} = {var}")) - var <- Fortran(tmp@name, tmp) + var <- materialize_via_hoist( + var, + mode = var@value@mode, + dims = var@value@dims, + hoist = hoist + ) } base_name <- var@value@name %||% stop("missing array name for subscripting") @@ -113,9 +150,6 @@ r2f_handlers[["["]] <- function( } idxs <- imap(idxs, function(subscript, i) { - # if (!idx@value@rank %in% 0:1) - # stop("all args to x[...] must have rank 0 or 1", - # deparse1(as.call(c(quote(`[`,args ))))) switch( paste0(subscript@value@mode, subscript@value@rank), logical0 = { @@ -125,13 +159,13 @@ r2f_handlers[["["]] <- function( # we convert to a temp integer vector, doing the equivalent of R's which() i <- scope_unique_var(scope, "integer") f <- glue("pack([({i}, {i}=1, size({subscript}))], {subscript})") - return(Fortran(f, Variable("int", NA))) + return(Fortran(f, Variable("integer", NA))) }, integer0 = { if (drop) { subscript } else { - Fortran(glue("{subscript}:{subscript}"), Variable("int", 1)) + Fortran(glue("{subscript}:{subscript}"), Variable("integer", 1)) } }, integer1 = { @@ -150,14 +184,9 @@ r2f_handlers[["["]] <- function( } if (is_call(r, quote(`:`)) && length(r) == 3L) { - scalar <- r2f(r[[2L]], scope, ..., hoist = hoist) - if (scalar@value@mode == "double") { - scalar <- Fortran( - glue("int({scalar}, kind=c_ptrdiff_t)"), - Variable("integer", scalar@value@dims) - ) - } - return(scalar) + return(cast_subscript_to_integer( + r2f(r[[2L]], scope, ..., hoist = hoist) + )) } if (is_call(r, quote(seq_len)) && length(r) == 2L) { @@ -173,21 +202,15 @@ r2f_handlers[["["]] <- function( if (is_call(r, quote(seq))) { info <- seq_like_parse("seq", as.list(r)[-1L], scope) - scalar <- r2f(info$from, scope, ..., hoist = hoist) - if (scalar@value@mode == "double") { - scalar <- Fortran( - glue("int({scalar}, kind=c_ptrdiff_t)"), - Variable("integer", scalar@value@dims) - ) - } - return(scalar) + return(cast_subscript_to_integer( + r2f(info$from, scope, ..., hoist = hoist) + )) } } subscript }, - # double0 = { }, - # double1 = { }, + # Doubles were already coerced to integer by lower_subscript_args(). stop( "all args to x[...] must be logical or integer of rank 0 or 1", deparse1(as.call(c(list(as.name("[")), args))) @@ -211,9 +234,12 @@ r2f_handlers[["["]] <- function( !passes_as_scalar(var@value) && is.null(var@value@name) ) { - tmp <- hoist$declare_tmp(mode = var@value@mode, dims = var@value@dims) - hoist$emit(glue("{tmp@name} = {var}")) - var <- Fortran(tmp@name, tmp) + var <- materialize_via_hoist( + var, + mode = var@value@mode, + dims = var@value@dims, + hoist = hoist + ) } # External logicals are passed as integer storage (0/1) and are "booleanized" diff --git a/R/scope.R b/R/scope.R index 0423e5a..a132159 100644 --- a/R/scope.R +++ b/R/scope.R @@ -37,7 +37,6 @@ names.quickr_ordered_env <- function(x) { all_names <- ls(envir = x, sorted = FALSE) ordered_names <- attr(x, "ordered_names", TRUE) if (!setequal(all_names, ordered_names)) { - warning("untracked name") stop("untracked name") } ordered_names @@ -56,52 +55,6 @@ print.quickr_ordered_env <- function(x, ...) { } -check_assignment_compatible <- function(target, value) { - if (is.null(value)) { - return() - } - stopifnot(exprs = { - inherits(target, Variable) - inherits(value, Variable) - passes_as_scalar(target) || - passes_as_scalar(value) || - target@rank == value@rank - }) -} - -# Reassignment cannot re-type a Fortran variable the way R promotes an R -# binding, so a value whose mode sits above the variable's on the lattice -# (logical < integer < double < complex) would be silently truncated by the -# assignment. Refuse at compile time instead. -check_reassignment_narrowing <- function(name, target, value) { - if ( - !inherits(target, Variable) || - !inherits(value, Variable) || - is.null(target@mode) || - is.null(value@mode) - ) { - return() - } - target_rank <- mode_rank(target@mode) - value_rank <- mode_rank(value@mode) - if (is.na(target_rank) || is.na(value_rank) || value_rank <= target_rank) { - return() - } - stop( - "cannot reassign `", - name, - "`: assignment would narrow ", - value@mode, - " to ", - target@mode, - "; R would promote `", - name, - "` to ", - value@mode, - call. = FALSE - ) -} - new_scope <- function(closure, parent = emptyenv()) { scope <- new_ordered_env(parent = parent) class(scope) <- unique(c("quickr_scope", class(scope))) @@ -148,7 +101,7 @@ new_scope <- function(closure, parent = emptyenv()) { name <- as.character(name) existing <- get0(name, scope) if (inherits(existing, Variable)) { - check_assignment_compatible(existing, value) + check_assignment_compatible(name, existing, value) } value@name <- name assign(name, value, scope) diff --git a/R/sizes.R b/R/sizes.R index bbf336d..d472d14 100644 --- a/R/sizes.R +++ b/R/sizes.R @@ -6,8 +6,22 @@ check_type_call <- function(cl) { if (length(names(args)) != 1) { stop("name must be provided as: type( = (<>)") } - if (!is.call(args[[1]]) && as.character(args[[1]]) %in% .atomic_type_names) { - stop("only atomic modes are supported") + mode_expr <- args[[1]] + mode_sym <- if (is.call(mode_expr)) mode_expr[[1L]] else mode_expr + if ( + !is.symbol(mode_sym) || + !as.character(mode_sym) %in% .atomic_type_names + ) { + stop("only atomic modes are supported, not: ", deparse1(mode_sym)) + } + if (!is.call(mode_expr)) { + stop( + "the mode must be a call with dimensions, as in: type(", + names(args), + " = ", + as.character(mode_sym), + "())" + ) } } @@ -15,10 +29,21 @@ check_type_call <- function(cl) { type_call_to_var <- function(cl) { check_type_call(cl) r_name <- names(cl)[-1] + mode <- as.character(cl[[2L]][[1L]]) + if (identical(mode, "character")) { + # No Fortran translation exists; refuse at the declaration instead of + # surfacing an internal error from the code generator. + stop( + "in declare(type(", + r_name, + " = character(...))): character values are not supported by quickr", + call. = FALSE + ) + } Variable( name = fortranize_name(r_name), r_name = r_name, - mode = as.character(cl[[2L]][[1L]]), + mode = mode, dims = unname(as.list(cl[[2]])[-1]) ) } @@ -131,10 +156,39 @@ substitute_declared_sizes <- function(e) { } -r2size <- function(r, scope) { +reject_local_closure_size_call <- function(r, scope) { + if (!is.call(r) || !is.symbol(r[[1L]]) || is.null(scope)) { + return(invisible(NULL)) + } + name <- as.character(r[[1L]]) + if (inherits(get0(name, scope), LocalClosure)) { + stop( + "local closure `", + name, + "()` cannot determine a result size before the generated function runs", + call. = FALSE + ) + } + invisible(NULL) +} + +unwrap_scalar_size_expr <- function(r, scope) { + repeat { + r <- unwrap_parens(r) + reject_local_closure_size_call(r, scope) + if (!is_call(r, quote(c)) || length(r) != 2L) { + return(r) + } + r <- r[[2L]] + } +} + +r2size <- function(r, scope, preserve_numeric = FALSE) { + r <- unwrap_scalar_size_expr(r, scope) + sanitize_dim <- function(dim) { if (is.symbol(dim) || is.call(dim)) { - return(r2size(dim, scope)) + return(r2size(dim, scope, preserve_numeric = preserve_numeric)) } dim } @@ -156,6 +210,8 @@ r2size <- function(r, scope) { double = { if (is_wholenumber(r)) { as.integer(r) + } else if (isTRUE(preserve_numeric)) { + r } else { stop("size must be an integer, found: ", r) } @@ -183,14 +239,19 @@ r2size <- function(r, scope) { # symbol, or fail gracefully and return NA. # closure-locals with unspecified shape are declared allocatable # input and/or output args with unspecified shape signal an error. - r2size(var@r, scope) + r2size(var@r, scope, preserve_numeric = preserve_numeric) }, language = { op <- as.character(r[[1]]) - if (op %in% c("+", "-", "/", "*", "^", "%/%", "%%")) { + if (op %in% c("+", "-", "/", "*", "^", "%/%", "%%", "abs")) { args <- as.list(r)[-1] - args <- lapply(args, r2size, scope) + args <- lapply( + args, + r2size, + scope, + preserve_numeric = preserve_numeric + ) if (anyNA(rapply(args, as.list))) { return(NA_integer_) } @@ -203,6 +264,46 @@ r2size <- function(r, scope) { switch( op, + as.integer = { + if (length(r) != 2L) { + stop("as.integer() in a size expression expects one argument") + } + inner_expr <- unwrap_scalar_size_expr(r[[2L]], scope) + # A scalar literal is coerced here rather than recursed into: + # r2size() rejects a non-whole double (and a bare logical), which + # are exactly the cases as.integer() exists to handle. + if ( + is.atomic(inner_expr) && + typeof(inner_expr) %in% c("logical", "integer", "double") && + length(inner_expr) == 1L + ) { + return(as.integer(inner_expr)) + } + # An explicit coercion is exactly what the "not an integer" + # warning asks for, so don't also warn about the operand. + inner <- withCallingHandlers( + r2size(inner_expr, scope, preserve_numeric = TRUE), + warning = function(w) { + if ( + grepl( + "size is not an integer", + conditionMessage(w), + fixed = TRUE + ) + ) { + invokeRestart("muffleWarning") + } + } + ) + if (is.atomic(inner) && length(inner) == 1L) { + if (is.na(inner)) { + return(NA_integer_) + } + # truncates toward zero, as as.integer() does in R + return(as.integer(inner)) + } + call("as.integer", inner) + }, length = { var <- get0(as.character(r[[2L]]), scope) if (!inherits(var, Variable)) { @@ -271,6 +372,26 @@ get_size_name <- function(var, axis = NULL, name = var@name, rank = var@rank) { } } +# TRUE when any of `var`'s dims is its own self-size symbol +# (`a__dim_1_`, `a__len_`), i.e. the variable was declared with unknown +# (NA) sizes that substitute_declared_sizes() rewrote. External +# variables receive those sizes as dummies; for locals they are +# phantoms, so the manifest declares such locals deferred-shape and +# relies on implicit allocation. +# Used by: manifest.R, check_assignment_compatible() +has_self_size_dims <- function(var) { + stopifnot(inherits(var, Variable)) + any(vapply( + seq_along(var@dims), + function(i) { + d <- var@dims[[i]] + is.symbol(d) && + identical(as.character(d), get_size_name(var, axis = i)) + }, + logical(1) + )) +} + # TODO: allow syntax like: # declare(type(a, b, c = integer(1))) # or: diff --git a/R/subroutine.R b/R/subroutine.R index 3382775..b37f271 100644 --- a/R/subroutine.R +++ b/R/subroutine.R @@ -98,7 +98,7 @@ new_fortran_subroutine <- function( uses_rng <- scope_uses_rng(scope) used_iso_bindings <- iso_c_binding_symbols( vars = scope_vars(scope), - body_code = body, + body_code = str_flatten_lines(manifest, body_code), logical_is_c_int = function(var) var@name %in% fsub_arg_names, uses_rng = uses_rng, include_errors = uses_errors diff --git a/tests/testthat/_snaps/bind.md b/tests/testthat/_snaps/bind.md index d6458b0..c49b875 100644 --- a/tests/testthat/_snaps/bind.md +++ b/tests/testthat/_snaps/bind.md @@ -3,9 +3,9 @@ Code capture_bind_error(r2f(bad_cbind)) Output - cbind() only supports rank 0-2 inputs + cbind() only supports rank 0-2 inputs Code capture_bind_error(r2f(bad_rbind)) Output - rbind() only supports rank 0-2 inputs + rbind() only supports rank 0-2 inputs diff --git a/tests/testthat/_snaps/block-scopes.md b/tests/testthat/_snaps/block-scopes.md index 4f92350..c4dc8fd 100644 --- a/tests/testthat/_snaps/block-scopes.md +++ b/tests/testthat/_snaps/block-scopes.md @@ -127,9 +127,9 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/c-bridge-hoist.md b/tests/testthat/_snaps/c-bridge-hoist.md index b866a3b..fd163de 100644 --- a/tests/testthat/_snaps/c-bridge-hoist.md +++ b/tests/testthat/_snaps/c-bridge-hoist.md @@ -22,23 +22,23 @@ cat(fsub) Output subroutine fn(n, m, a, b, out) bind(c) - use iso_c_binding, only: c_double, c_int + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t implicit none ! manifest start ! args integer(c_int), intent(in) :: n integer(c_int), intent(in) :: m - real(c_double), intent(in) :: a(min(n, m)) - real(c_double), intent(in) :: b(min(n, m)) - real(c_double), intent(out) :: out(min(n, m)) + real(c_double), intent(in) :: a(int((min(real(n, kind=c_double), real(m, kind=c_double))), kind=c_ptrdiff_t)) + real(c_double), intent(in) :: b(int((min(real(n, kind=c_double), real(m, kind=c_double))), kind=c_ptrdiff_t)) + real(c_double), intent(out) :: out(int((min(real(n, kind=c_double), real(m, kind=c_double))), kind=c_ptrdiff_t)) ! locals integer(c_int) :: i ! manifest end - out = 0 + out = 0.0_c_double do i = 1, size(out) out(i) = (a(i) + b(i)) end do @@ -52,10 +52,10 @@ extern void fn( - const int* const n__, - const int* const m__, - const double* const a__, - const double* const b__, + const int* const n__, + const int* const m__, + const double* const a__, + const double* const b__, double* const out__); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/closure-hoist-snapshots.md b/tests/testthat/_snaps/closure-hoist-snapshots.md index e2fe6a3..cab59e0 100644 --- a/tests/testthat/_snaps/closure-hoist-snapshots.md +++ b/tests/testthat/_snaps/closure-hoist-snapshots.md @@ -34,7 +34,7 @@ ! manifest end - out = 0 + out = 0.0_c_double do tmp1_ = 1_c_int, x__len_ call closure1_(tmp1_, out(tmp1_)) @@ -68,8 +68,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -166,8 +166,8 @@ extern void fn( - const int* const nx__, - const int* const ny__, + const int* const nx__, + const int* const ny__, double* const temp__); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/dims2c-length.md b/tests/testthat/_snaps/dims2c-length.md index 2ce55d8..3cc8f26 100644 --- a/tests/testthat/_snaps/dims2c-length.md +++ b/tests/testthat/_snaps/dims2c-length.md @@ -41,8 +41,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -109,9 +109,9 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { @@ -166,14 +166,14 @@ cat(fsub) Output subroutine fn(n, m, out) bind(c) - use iso_c_binding, only: c_double, c_int + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t implicit none ! manifest start ! args integer(c_int), intent(in) :: n integer(c_int), intent(in) :: m - real(c_double), intent(out) :: out(min(n, m)) + real(c_double), intent(out) :: out(int((min(real(n, kind=c_double), real(m, kind=c_double))), kind=c_ptrdiff_t)) ! locals integer(c_int) :: i @@ -193,8 +193,8 @@ extern void fn( - const int* const n__, - const int* const m__, + const int* const n__, + const int* const m__, double* const out__); SEXP fn_(SEXP _args) { @@ -255,14 +255,14 @@ cat(fsub) Output subroutine fn(n, m, out) bind(c) - use iso_c_binding, only: c_double, c_int + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t implicit none ! manifest start ! args integer(c_int), intent(in) :: n integer(c_int), intent(in) :: m - real(c_double), intent(out) :: out(max(n, m)) + real(c_double), intent(out) :: out(int((max(real(n, kind=c_double), real(m, kind=c_double))), kind=c_ptrdiff_t)) ! locals integer(c_int) :: i @@ -282,8 +282,8 @@ extern void fn( - const int* const n__, - const int* const m__, + const int* const n__, + const int* const m__, double* const out__); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/dims2f.md b/tests/testthat/_snaps/dims2f.md index dcbc008..b6266ed 100644 --- a/tests/testthat/_snaps/dims2f.md +++ b/tests/testthat/_snaps/dims2f.md @@ -31,8 +31,8 @@ allocate(y((n - 1))) - x = 0 - y = 0 + x = 0.0_c_double + y = 0.0_c_double out_ = (size(x) + size(y)) end subroutine Code @@ -83,7 +83,7 @@ cat(fsub) Output subroutine fn(n, out_) bind(c) - use iso_c_binding, only: c_double, c_int + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t implicit none ! manifest start @@ -95,10 +95,12 @@ real(c_double), allocatable :: out(:) ! manifest end - allocate(out((int(n) / int(2) + mod(int(n), int(2))))) + allocate(out(int((((aint((real(n, kind=c_double) / real(2, kind=c_double))) - merge(1.0_c_double, 0.0_c_double, ((real(n,& + & kind=c_double) / real(2, kind=c_double)) < aint((real(n, kind=c_double) / real(2, kind=c_double)))))) + modulo(real(n,& + & kind=c_double), real(2, kind=c_double)))), kind=c_ptrdiff_t))) - out = 0 + out = 0.0_c_double out_ = size(out) end subroutine Code @@ -149,7 +151,7 @@ cat(fsub) Output subroutine fn(n, out_) bind(c) - use iso_c_binding, only: c_double, c_int + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t implicit none ! manifest start @@ -161,7 +163,8 @@ real(c_double), allocatable :: out(:, :) ! manifest end - allocate(out((n + 1), (int(n) / int(2) + 1))) + allocate(out((n + 1), int((((aint((real(n, kind=c_double) / real(2, kind=c_double))) - merge(1.0_c_double, 0.0_c_double, ((real(n,& + & kind=c_double) / real(2, kind=c_double)) < aint((real(n, kind=c_double) / real(2, kind=c_double)))))) + 1)), kind=c_ptrdiff_t))) out = 1.0_c_double diff --git a/tests/testthat/_snaps/div-cast.md b/tests/testthat/_snaps/div-cast.md index 1b13ef5..7ef66ab 100644 --- a/tests/testthat/_snaps/div-cast.md +++ b/tests/testthat/_snaps/div-cast.md @@ -37,9 +37,9 @@ extern void fn( - const int* const a__, - const int* const b__, - double* const out___, + const int* const a__, + const int* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -118,9 +118,9 @@ extern void fn( - const double* const a__, - const int* const b__, - double* const out___, + const double* const a__, + const int* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -199,9 +199,9 @@ extern void fn( - const double* const a__, - const int* const b__, - double* const out___, + const double* const a__, + const int* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -277,8 +277,8 @@ extern void fn( - const int* const a__, - const int* const b__, + const int* const a__, + const int* const b__, double* const out___); SEXP fn_(SEXP _args) { @@ -355,9 +355,9 @@ extern void fn( - const Rcomplex* const a__, - const Rcomplex* const b__, - Rcomplex* const out___, + const Rcomplex* const a__, + const Rcomplex* const b__, + Rcomplex* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -436,8 +436,8 @@ extern void fn( - const double* const x__, - double* const mu__, + const double* const x__, + double* const mu__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/div-mod.md b/tests/testthat/_snaps/div-mod.md index ab5e66b..8112855 100644 --- a/tests/testthat/_snaps/div-mod.md +++ b/tests/testthat/_snaps/div-mod.md @@ -37,9 +37,9 @@ extern void fn( - const double* const a__, - const double* const b__, - double* const out___, + const double* const a__, + const double* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -124,9 +124,9 @@ extern void fn( - const double* const a__, - const double* const b__, - double* const out___, + const double* const a__, + const double* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -202,8 +202,8 @@ extern void fn( - const int* const a__, - const int* const b__, + const int* const a__, + const int* const b__, int* const out___); SEXP fn_(SEXP _args) { @@ -282,8 +282,8 @@ extern void fn( - const double* const a__, - const double* const b__, + const double* const a__, + const double* const b__, double* const out___); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/drop.md b/tests/testthat/_snaps/drop.md index 3891d24..c12c961 100644 --- a/tests/testthat/_snaps/drop.md +++ b/tests/testthat/_snaps/drop.md @@ -174,8 +174,8 @@ extern void fn( - const double* const A__, - double* const out___, + const double* const A__, + double* const out___, const R_len_t A__dim_2_); SEXP fn_(SEXP _args) { @@ -246,8 +246,8 @@ extern void fn( - const double* const A__, - double* const out___, + const double* const A__, + double* const out___, const R_len_t A__dim_1_); SEXP fn_(SEXP _args) { @@ -318,8 +318,8 @@ extern void fn( - const double* const A__, - const int* const n__, + const double* const A__, + const int* const n__, double* const out___); SEXP fn_(SEXP _args) { @@ -407,8 +407,8 @@ extern void fn( - const double* const A__, - const int* const n__, + const double* const A__, + const int* const n__, double* const out___); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/error-handling.md b/tests/testthat/_snaps/error-handling.md index 292eaa0..95f2e98 100644 --- a/tests/testthat/_snaps/error-handling.md +++ b/tests/testthat/_snaps/error-handling.md @@ -59,8 +59,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, char* quickr_err_msg); SEXP fn_(SEXP _args) { @@ -151,8 +151,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, char* quickr_err_msg); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/example-convolve.md b/tests/testthat/_snaps/example-convolve.md index 2894c0e..820432f 100644 --- a/tests/testthat/_snaps/example-convolve.md +++ b/tests/testthat/_snaps/example-convolve.md @@ -40,7 +40,7 @@ - ab = 0 + ab = 0.0_c_double do i = 1, size(a) do j = 1, size(b) ab(((i + j) - 1_c_int)) = (ab(((i + j) - 1_c_int)) + (a(i) * b(j))) @@ -56,10 +56,10 @@ extern void slow_convolve( - const double* const a__, - const double* const b__, - double* const ab__, - const R_xlen_t a__len_, + const double* const a__, + const double* const b__, + double* const ab__, + const R_xlen_t a__len_, const R_xlen_t b__len_); SEXP slow_convolve_(SEXP _args) { diff --git a/tests/testthat/_snaps/example-heat_diffusion.md b/tests/testthat/_snaps/example-heat_diffusion.md index a12feea..abab20d 100644 --- a/tests/testthat/_snaps/example-heat_diffusion.md +++ b/tests/testthat/_snaps/example-heat_diffusion.md @@ -150,13 +150,13 @@ extern void diffuse_heat( - const int* const nx__, - const int* const ny__, - const int* const dx__, - const int* const dy__, - const double* const dt__, - const double* const k__, - const int* const steps__, + const int* const nx__, + const int* const ny__, + const int* const dx__, + const int* const dy__, + const double* const dt__, + const double* const k__, + const int* const steps__, double* const temp__); SEXP diffuse_heat_(SEXP _args) { @@ -406,13 +406,13 @@ extern void diffuse_heat( - const int* const nx__, - const int* const ny__, - const int* const dx__, - const int* const dy__, - const double* const dt__, - const double* const k__, - const int* const steps__, + const int* const nx__, + const int* const ny__, + const int* const dx__, + const int* const dy__, + const double* const dt__, + const double* const k__, + const int* const steps__, double* const temp__); SEXP diffuse_heat_(SEXP _args) { diff --git a/tests/testthat/_snaps/example-roll_mean.md b/tests/testthat/_snaps/example-roll_mean.md index da122d2..cc9f832 100644 --- a/tests/testthat/_snaps/example-roll_mean.md +++ b/tests/testthat/_snaps/example-roll_mean.md @@ -24,8 +24,8 @@ Code cat(fsub) Output - subroutine fn(x, weights, normalize, out, weights__len_, x__len_) bind(c) - use iso_c_binding, only: c_double, c_int, c_ptrdiff_t + subroutine fn(x, weights, normalize, out, weights__len_, x__len_, quickr_err_msg) bind(c) + use iso_c_binding, only: c_char, c_double, c_int, c_null_char, c_ptrdiff_t implicit none ! manifest start @@ -33,6 +33,9 @@ integer(c_ptrdiff_t), intent(in), value :: x__len_ integer(c_ptrdiff_t), intent(in), value :: weights__len_ + ! error + character(kind=c_char), intent(inout) :: quickr_err_msg(256) + ! args real(c_double), intent(in) :: x(x__len_) real(c_double), intent(in out) :: weights(weights__len_) @@ -45,14 +48,31 @@ ! manifest end - out = 0 + out = 0.0_c_double n = size(weights) if ((normalize/=0)) then weights = ((weights / sum(weights)) * size(weights)) end if do i = 1, size(out) + if (size(x(i:(((i + n) - 1_c_int)):sign(1, (((i + n) - 1_c_int))-i)), kind=c_ptrdiff_t) /= size(weights, kind=c_ptrdiff_t)) then + call quickr_set_error_msg("elementwise vector operations require equal lengths or a scalar operand; R-style recycling is not& + & supported") + return + end if out(i) = (sum((x(i:(((i + n) - 1_c_int)):sign(1, (((i + n) - 1_c_int))-i)) * weights)) / real(size(weights), kind=c_double)) end do + + contains + subroutine quickr_set_error_msg(msg) + character(len=*), intent(in) :: msg + integer :: i + integer :: n + if (quickr_err_msg(1) == c_null_char) then + n = min(len(msg), 256 - 1) + quickr_err_msg(1:n) = [(msg(i:i), i = 1, n)] + quickr_err_msg(n + 1) = c_null_char + end if + end subroutine quickr_set_error_msg end subroutine Code cat(cwrapper) @@ -63,12 +83,13 @@ extern void fn( - const double* const x__, - double* const weights__, - const int* const normalize__, - double* const out__, - const R_xlen_t weights__len_, - const R_xlen_t x__len_); + const double* const x__, + double* const weights__, + const int* const normalize__, + double* const out__, + const R_xlen_t weights__len_, + const R_xlen_t x__len_, + char* quickr_err_msg); SEXP fn_(SEXP _args) { // x @@ -107,13 +128,21 @@ SEXP out = PROTECT(Rf_allocVector(REALSXP, out__len_)); double* out__ = REAL(out); + char quickr_err_msg[256]; + quickr_err_msg[0] = '\0'; + + fn( x__, weights__, normalize__, out__, weights__len_, - x__len_); + x__len_, + quickr_err_msg); + if (quickr_err_msg[0] != '\0') { + Rf_error("%s", quickr_err_msg); + } UNPROTECT(1); return out; diff --git a/tests/testthat/_snaps/example-viterbi.md b/tests/testthat/_snaps/example-viterbi.md index ae2961b..45a0f31 100644 --- a/tests/testthat/_snaps/example-viterbi.md +++ b/tests/testthat/_snaps/example-viterbi.md @@ -122,7 +122,7 @@ backpointer(current_state, step) = maxloc(probabilities, 1) end do end do - path = 0 + path = 0_c_int path(num_steps) = maxloc(trellis(:, num_steps), 1) do step = ((num_steps - 1_c_int)), 1_c_int, sign(1, 1_c_int-((num_steps - 1_c_int))) path(step) = backpointer(path((step + 1_c_int)), (step + 1_c_int)) @@ -138,14 +138,14 @@ extern void viterbi( - const int* const observations__, - const int* const states__, - const double* const initial_probs__, - const double* const transition_probs__, - const double* const emission_probs__, - int* const out__, - const R_len_t emission_probs__dim_2_, - const R_xlen_t observations__len_, + const int* const observations__, + const int* const states__, + const double* const initial_probs__, + const double* const transition_probs__, + const double* const emission_probs__, + int* const out__, + const R_len_t emission_probs__dim_2_, + const R_xlen_t observations__len_, const R_xlen_t states__len_); SEXP viterbi_(SEXP _args) { @@ -337,7 +337,7 @@ backpointer(current_state, step) = maxloc(probabilities, 1) end do end do - path = 0 + path = 0_c_int path(size(observations)) = maxloc(trellis(:, size(observations)), 1) do step = (size(observations) - 1_c_int), 1_c_int, sign(1, 1_c_int-(size(observations) - 1_c_int)) path(step) = backpointer(path((step + 1_c_int)), (step + 1_c_int)) @@ -353,14 +353,14 @@ extern void viterbi( - const int* const observations__, - const int* const states__, - const double* const initial_probs__, - const double* const transition_probs__, - const double* const emission_probs__, - int* const out__, - const R_len_t emission_probs__dim_2_, - const R_xlen_t observations__len_, + const int* const observations__, + const int* const states__, + const double* const initial_probs__, + const double* const transition_probs__, + const double* const emission_probs__, + int* const out__, + const R_len_t emission_probs__dim_2_, + const R_xlen_t observations__len_, const R_xlen_t states__len_); SEXP viterbi_(SEXP _args) { diff --git a/tests/testthat/_snaps/float-to-int.md b/tests/testthat/_snaps/float-to-int.md index b8cc9c1..ea67958 100644 --- a/tests/testthat/_snaps/float-to-int.md +++ b/tests/testthat/_snaps/float-to-int.md @@ -37,8 +37,8 @@ extern void fn( - const double* const x__, - int* const out__, + const double* const x__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -100,8 +100,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -163,8 +163,8 @@ extern void fn( - const int* const x__, - double* const out__, + const int* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -227,9 +227,9 @@ extern void fn( - const int* const a__, - const int* const b__, - int* const out__, + const int* const a__, + const int* const b__, + int* const out__, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/hoist-mask.md b/tests/testthat/_snaps/hoist-mask.md index 306ea92..e0f1df1 100644 --- a/tests/testthat/_snaps/hoist-mask.md +++ b/tests/testthat/_snaps/hoist-mask.md @@ -37,8 +37,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -100,8 +100,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/ifelse.md b/tests/testthat/_snaps/ifelse.md index f9798be..2d84fc0 100644 --- a/tests/testthat/_snaps/ifelse.md +++ b/tests/testthat/_snaps/ifelse.md @@ -37,9 +37,9 @@ extern void fn( - const int* const c__, - const double* const a__, - double* const out___, + const int* const c__, + const double* const a__, + double* const out___, const R_xlen_t c__len_); SEXP fn_(SEXP _args) { @@ -113,12 +113,12 @@ ! manifest end - if (size(a, 1) /= size((c/=0), 1)) then + if (size(a, 1, kind=c_ptrdiff_t) /= size((c/=0), 1, kind=c_ptrdiff_t)) then call quickr_set_error_msg("ifelse() `yes` and `no` must be scalars or match the shape of `test`; R-style recycling is not& & supported") return end if - if (size(b, 1) /= size((c/=0), 1)) then + if (size(b, 1, kind=c_ptrdiff_t) /= size((c/=0), 1, kind=c_ptrdiff_t)) then call quickr_set_error_msg("ifelse() `yes` and `no` must be scalars or match the shape of `test`; R-style recycling is not& & supported") return @@ -146,13 +146,13 @@ extern void fn( - const int* const c__, - const double* const a__, - const double* const b__, - double* const out___, - const R_xlen_t a__len_, - const R_xlen_t b__len_, - const R_xlen_t c__len_, + const int* const c__, + const double* const a__, + const double* const b__, + double* const out___, + const R_xlen_t a__len_, + const R_xlen_t b__len_, + const R_xlen_t c__len_, char* quickr_err_msg); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/logical-indexing.md b/tests/testthat/_snaps/logical-indexing.md index aef45ec..591a0e5 100644 --- a/tests/testthat/_snaps/logical-indexing.md +++ b/tests/testthat/_snaps/logical-indexing.md @@ -28,7 +28,7 @@ ! manifest end - out = 0 + out = 0.0_c_double out = merge(1.0_c_double, 0.0_c_double, (pred(2_c_int, 3_c_int) /= 0)) end subroutine Code @@ -104,7 +104,7 @@ ! manifest end - out = 0 + out = 0.0_c_double block logical :: btmp1_(3, 4) ! logical @@ -185,7 +185,7 @@ ! manifest end - out = 0 + out = 0.0_c_double block logical :: btmp1_(3, 4) ! logical @@ -274,7 +274,7 @@ ! manifest end - out = 0 + out = 0.0_c_double out = sum(((x + y)), mask = (z > a)) end subroutine Code @@ -286,10 +286,10 @@ extern void fn( - const double* const x__, - const double* const y__, - const double* const z__, - const double* const a__, + const double* const x__, + const double* const y__, + const double* const z__, + const double* const a__, double* const out__); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/logical.md b/tests/testthat/_snaps/logical.md index af76936..a239f7c 100644 --- a/tests/testthat/_snaps/logical.md +++ b/tests/testthat/_snaps/logical.md @@ -43,10 +43,10 @@ extern void fn( - const double* const x__, - const double* const left__, - const double* const right__, - int* const out__, + const double* const x__, + const double* const left__, + const double* const right__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -161,8 +161,8 @@ extern void fn( - const double* const a__, - const double* const b__, + const double* const a__, + const double* const b__, int* const out__); SEXP fn_(SEXP _args) { @@ -246,8 +246,8 @@ extern void fn( - const double* const a__, - const double* const b__, + const double* const a__, + const double* const b__, int* const out__); SEXP fn_(SEXP _args) { @@ -322,8 +322,8 @@ extern void fn( - const double* const a__, - const double* const b__, + const double* const a__, + const double* const b__, int* const out__); SEXP fn_(SEXP _args) { @@ -401,9 +401,9 @@ extern void fn( - const double* const a__, - const double* const b__, - int* const out__, + const double* const a__, + const double* const b__, + int* const out__, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -480,8 +480,8 @@ extern void fn( - const int* const x__, - const int* const y__, + const int* const x__, + const int* const y__, int* const cond__); SEXP fn_(SEXP _args) { @@ -563,8 +563,8 @@ extern void fn( - const int* const x__, - const int* const y__, + const int* const x__, + const int* const y__, int* const out___); SEXP fn_(SEXP _args) { @@ -602,3 +602,176 @@ return out_; } +# && and || short-circuit like R's scalar operators + + Code + fn + Output + function(i, x) { + declare(type(i = integer(1)), type(x = double(3))) + out <- 0 + if (i <= 3L && x[i] > 0) { + out <- 1 + } + out + } + + Code + cat(fsub) + Output + subroutine fn(i, x, out) bind(c) + use iso_c_binding, only: c_double, c_int + implicit none + + ! manifest start + ! args + integer(c_int), intent(in) :: i + real(c_double), intent(in) :: x(3) + real(c_double), intent(out) :: out + ! manifest end + + + out = 0.0_c_double + block + logical :: btmp1_ ! logical + + btmp1_ = (i <= 3_c_int) + if (btmp1_) then + btmp1_ = (x(i) > 0.0_c_double) + end if + if (btmp1_) then + out = 1.0_c_double + end if + end block + end subroutine + Code + cat(cwrapper) + Output + #define R_NO_REMAP + #include + #include + + + extern void fn( + const int* const i__, + const double* const x__, + double* const out__); + + SEXP fn_(SEXP _args) { + // i + _args = CDR(_args); + SEXP i = CAR(_args); + if (TYPEOF(i) != INTSXP) { + Rf_error("typeof(i) must be 'integer', not '%s'", Rf_type2char(TYPEOF(i))); + } + const int* const i__ = INTEGER(i); + const R_xlen_t i__len_ = Rf_xlength(i); + + // x + _args = CDR(_args); + SEXP x = CAR(_args); + if (TYPEOF(x) != REALSXP) { + Rf_error("typeof(x) must be 'double', not '%s'", Rf_type2char(TYPEOF(x))); + } + const double* const x__ = REAL(x); + const R_xlen_t x__len_ = Rf_xlength(x); + + if (i__len_ != 1) + Rf_error("length(i) must be 1, not %0.f", + (double)i__len_); + if (x__len_ != 3) + Rf_error("length(x) must be 3, not %0.f", + (double)x__len_); + const R_xlen_t out__len_ = (1); + SEXP out = PROTECT(Rf_allocVector(REALSXP, out__len_)); + double* out__ = REAL(out); + + fn(i__, x__, out__); + + UNPROTECT(1); + return out; + } + +# while re-evaluates hoisted condition code every iteration + + Code + fn + Output + function(x) { + declare(type(x = double(n))) + i <- 1L + n <- length(x) + while (i <= n && x[i] > 0) { + i <- i + 1L + } + i + } + + Code + cat(fsub) + Output + subroutine fn(x, i, x__len_) bind(c) + use iso_c_binding, only: c_double, c_int, c_ptrdiff_t + implicit none + + ! manifest start + ! sizes + integer(c_ptrdiff_t), intent(in), value :: x__len_ + + ! args + real(c_double), intent(in) :: x(x__len_) + integer(c_int), intent(out) :: i + + ! locals + integer(c_int) :: n + ! manifest end + + + i = 1_c_int + n = size(x) + do + block + logical :: btmp1_ ! logical + + btmp1_ = (i <= n) + if (btmp1_) then + btmp1_ = (x(i) > 0.0_c_double) + end if + if (.not. (btmp1_)) exit + end block + i = (i + 1_c_int) + end do + end subroutine + Code + cat(cwrapper) + Output + #define R_NO_REMAP + #include + #include + + + extern void fn( + const double* const x__, + int* const i__, + const R_xlen_t x__len_); + + SEXP fn_(SEXP _args) { + // x + _args = CDR(_args); + SEXP x = CAR(_args); + if (TYPEOF(x) != REALSXP) { + Rf_error("typeof(x) must be 'double', not '%s'", Rf_type2char(TYPEOF(x))); + } + const double* const x__ = REAL(x); + const R_xlen_t x__len_ = Rf_xlength(x); + + const R_xlen_t i__len_ = (1); + SEXP i = PROTECT(Rf_allocVector(INTSXP, i__len_)); + int* i__ = INTEGER(i); + + fn(x__, i__, x__len_); + + UNPROTECT(1); + return i; + } + diff --git a/tests/testthat/_snaps/loops.md b/tests/testthat/_snaps/loops.md index da48153..a419c18 100644 --- a/tests/testthat/_snaps/loops.md +++ b/tests/testthat/_snaps/loops.md @@ -437,8 +437,8 @@ extern void fn( - const int* const x__, - int* const out___, + const int* const x__, + int* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -461,3 +461,153 @@ return out_; } +# single-statement while/repeat bodies re-run their hoisted statements + + Code + fn + Output + function(m) { + declare(type(m = double(2, 2))) + while (m[1, 1] < 100) m <- m %*% m + m + } + + Code + cat(fsub) + Output + subroutine fn(m) bind(c) + use iso_c_binding, only: c_double, c_int + implicit none + + ! manifest start + ! args + real(c_double), intent(in out) :: m(2, 2) + ! manifest end + + + do while ((m(1_c_int, 1_c_int) < 100.0_c_double)) + block + real(c_double) :: btmp1_(2, 2) + + call dgemm('N','N', int(2, kind=c_int), int(2, kind=c_int), int(2, kind=c_int), 1.0_c_double, m, int(2, kind=c_int), m, int(2,& + & kind=c_int), 0.0_c_double, btmp1_, int(2, kind=c_int)) + m = btmp1_ + end block + end do + end subroutine + Code + cat(cwrapper) + Output + #define R_NO_REMAP + #include + #include + + + extern void fn(double* const m__); + + SEXP fn_(SEXP _args) { + // m + _args = CDR(_args); + SEXP m = CAR(_args); + if (TYPEOF(m) != REALSXP) { + Rf_error("typeof(m) must be 'double', not '%s'", Rf_type2char(TYPEOF(m))); + } + m = Rf_duplicate(m); + SETCAR(_args, m); + double* const m__ = REAL(m); + const int* const m__dim_ = ({ + SEXP dim_ = Rf_getAttrib(m, R_DimSymbol); + if (Rf_length(dim_) != 2) Rf_error( + "m must be a 2D-array, but length(dim(m)) is %i", + (int) Rf_length(dim_)); + INTEGER(dim_);}); + const int m__dim_1_ = m__dim_[0]; + const int m__dim_2_ = m__dim_[1]; + + if (m__dim_1_ != 2) + Rf_error("dim(m)[1] must be 2, not %0.f", + (double)m__dim_1_); + if (m__dim_2_ != 2) + Rf_error("dim(m)[2] must be 2, not %0.f", + (double)m__dim_2_); + + fn(m__); + + return m; + } + +--- + + Code + fn + Output + function(m) { + declare(type(m = double(2, 2))) + repeat m <- m %*% m + m + } + + Code + cat(fsub) + Output + subroutine fn(m) bind(c) + use iso_c_binding, only: c_double, c_int + implicit none + + ! manifest start + ! args + real(c_double), intent(in out) :: m(2, 2) + ! manifest end + + + do + block + real(c_double) :: btmp1_(2, 2) + + call dgemm('N','N', int(2, kind=c_int), int(2, kind=c_int), int(2, kind=c_int), 1.0_c_double, m, int(2, kind=c_int), m, int(2,& + & kind=c_int), 0.0_c_double, btmp1_, int(2, kind=c_int)) + m = btmp1_ + end block + end do + end subroutine + Code + cat(cwrapper) + Output + #define R_NO_REMAP + #include + #include + + + extern void fn(double* const m__); + + SEXP fn_(SEXP _args) { + // m + _args = CDR(_args); + SEXP m = CAR(_args); + if (TYPEOF(m) != REALSXP) { + Rf_error("typeof(m) must be 'double', not '%s'", Rf_type2char(TYPEOF(m))); + } + m = Rf_duplicate(m); + SETCAR(_args, m); + double* const m__ = REAL(m); + const int* const m__dim_ = ({ + SEXP dim_ = Rf_getAttrib(m, R_DimSymbol); + if (Rf_length(dim_) != 2) Rf_error( + "m must be a 2D-array, but length(dim(m)) is %i", + (int) Rf_length(dim_)); + INTEGER(dim_);}); + const int m__dim_1_ = m__dim_[0]; + const int m__dim_2_ = m__dim_[1]; + + if (m__dim_1_ != 2) + Rf_error("dim(m)[1] must be 2, not %0.f", + (double)m__dim_1_); + if (m__dim_2_ != 2) + Rf_error("dim(m)[2] must be 2, not %0.f", + (double)m__dim_2_); + + fn(m__); + + return m; + } + diff --git a/tests/testthat/_snaps/matrix.md b/tests/testthat/_snaps/matrix.md index 8fa7d81..711d6a6 100644 --- a/tests/testthat/_snaps/matrix.md +++ b/tests/testthat/_snaps/matrix.md @@ -178,9 +178,9 @@ extern void fn( - const double* const a1__, - const double* const a2__, - double* const out__, + const double* const a1__, + const double* const a2__, + double* const out__, const R_xlen_t a1__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/openmp-error-snapshots.md b/tests/testthat/_snaps/openmp-error-snapshots.md index daa2412..be7e971 100644 --- a/tests/testthat/_snaps/openmp-error-snapshots.md +++ b/tests/testthat/_snaps/openmp-error-snapshots.md @@ -73,8 +73,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, char* quickr_err_msg); SEXP fn_(SEXP _args) { @@ -194,8 +194,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, char* quickr_err_msg); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/parentheses.md b/tests/testthat/_snaps/parentheses.md index c4affb4..a55c3ed 100644 --- a/tests/testthat/_snaps/parentheses.md +++ b/tests/testthat/_snaps/parentheses.md @@ -42,10 +42,10 @@ extern void fn( - const double* const a__, - const double* const b__, - const double* const c__, - const double* const d__, + const double* const a__, + const double* const b__, + const double* const c__, + const double* const d__, double* const out___); SEXP fn_(SEXP _args) { @@ -151,9 +151,9 @@ extern void fn( - const double* const x__, - const double* const y__, - double* const out___, + const double* const x__, + const double* const y__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/qr-solve.md b/tests/testthat/_snaps/qr-solve.md index 88996bd..e6685f9 100644 --- a/tests/testthat/_snaps/qr-solve.md +++ b/tests/testthat/_snaps/qr-solve.md @@ -19,7 +19,7 @@ cat(fsub) Output subroutine fn(a, b, out_, a__dim_1_, a__dim_2_, quickr_err_msg) bind(c) - use iso_c_binding, only: c_char, c_double, c_int, c_null_char + use iso_c_binding, only: c_char, c_double, c_int, c_null_char, c_ptrdiff_t implicit none ! manifest start @@ -54,7 +54,15 @@ allocate(btmp3_(a__dim_2_)) allocate(btmp4_(a__dim_2_)) allocate(btmp5_(a__dim_2_, 2)) - allocate(btmp8_(min(a__dim_1_, a__dim_2_), 1)) + allocate(btmp8_(int((min(real(a__dim_1_, kind=c_double), real(a__dim_2_, kind=c_double))), kind=c_ptrdiff_t), 1)) + if (size(a, 1, kind=c_ptrdiff_t) == 0_c_ptrdiff_t) then + call quickr_set_error_msg("qr.solve coefficient matrices with zero extents are not supported") + return + end if + if (size(a, 2, kind=c_ptrdiff_t) == 0_c_ptrdiff_t) then + call quickr_set_error_msg("qr.solve coefficient matrices with zero extents are not supported") + return + end if btmp1_ = a btmp2_ = 0.0_c_double btmp2_(1:a__dim_1_, 1) = b @@ -100,11 +108,11 @@ extern void fn( - const double* const a__, - const double* const b__, - double* const out___, - const R_len_t a__dim_1_, - const R_len_t a__dim_2_, + const double* const a__, + const double* const b__, + double* const out___, + const R_len_t a__dim_1_, + const R_len_t a__dim_2_, char* quickr_err_msg); SEXP fn_(SEXP _args) { @@ -181,7 +189,7 @@ cat(fsub) Output subroutine fn(a, b, out_, a__dim_1_, a__dim_2_, b__dim_2_, quickr_err_msg) bind(c) - use iso_c_binding, only: c_char, c_double, c_int, c_null_char + use iso_c_binding, only: c_char, c_double, c_int, c_null_char, c_ptrdiff_t implicit none ! manifest start @@ -218,7 +226,15 @@ allocate(btmp3_(a__dim_2_)) allocate(btmp4_(a__dim_2_)) allocate(btmp5_(a__dim_2_, 2)) - allocate(btmp8_(min(a__dim_1_, a__dim_2_), b__dim_2_)) + allocate(btmp8_(int((min(real(a__dim_1_, kind=c_double), real(a__dim_2_, kind=c_double))), kind=c_ptrdiff_t), b__dim_2_)) + if (size(a, 1, kind=c_ptrdiff_t) == 0_c_ptrdiff_t) then + call quickr_set_error_msg("qr.solve coefficient matrices with zero extents are not supported") + return + end if + if (size(a, 2, kind=c_ptrdiff_t) == 0_c_ptrdiff_t) then + call quickr_set_error_msg("qr.solve coefficient matrices with zero extents are not supported") + return + end if btmp1_ = a btmp2_ = 0.0_c_double btmp2_(1:a__dim_1_, 1:b__dim_2_) = b @@ -232,10 +248,12 @@ return end if btmp8_ = 0.0_c_double - call dqrcf(btmp1_, int(a__dim_1_, kind=c_int), btmp6_, btmp3_, btmp2_, int(b__dim_2_, kind=c_int), btmp8_, btmp9_) - if (btmp9_ /= 0_c_int) then - call quickr_set_error_msg("exact singularity in 'qr.coef'") - return + if (int(b__dim_2_, kind=c_int) > 0_c_int) then + call dqrcf(btmp1_, int(a__dim_1_, kind=c_int), btmp6_, btmp3_, btmp2_, int(b__dim_2_, kind=c_int), btmp8_, btmp9_) + if (btmp9_ /= 0_c_int) then + call quickr_set_error_msg("exact singularity in 'qr.coef'") + return + end if end if out_ = 0.0_c_double do btmp11_ = 1_c_int, int(b__dim_2_, kind=c_int) @@ -266,12 +284,12 @@ extern void fn( - const double* const a__, - const double* const b__, - double* const out___, - const R_len_t a__dim_1_, - const R_len_t a__dim_2_, - const R_len_t b__dim_2_, + const double* const a__, + const double* const b__, + double* const out___, + const R_len_t a__dim_1_, + const R_len_t a__dim_2_, + const R_len_t b__dim_2_, char* quickr_err_msg); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/runif.md b/tests/testthat/_snaps/runif.md index 508a128..77a5e5a 100644 --- a/tests/testthat/_snaps/runif.md +++ b/tests/testthat/_snaps/runif.md @@ -117,8 +117,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -192,8 +192,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -270,9 +270,9 @@ extern void fn( - const int* const n__, - const double* const a__, - const double* const b__, + const int* const n__, + const double* const a__, + const double* const b__, double* const out___); SEXP fn_(SEXP _args) { @@ -379,8 +379,8 @@ extern void fn( - const int* const n__, - const double* const b__, + const int* const n__, + const double* const b__, double* const out___); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/sapply-closures.md b/tests/testthat/_snaps/sapply-closures.md index ffa1f67..eb026d3 100644 --- a/tests/testthat/_snaps/sapply-closures.md +++ b/tests/testthat/_snaps/sapply-closures.md @@ -35,7 +35,7 @@ ! manifest end - out = 0 + out = 0.0_c_double do tmp1_ = 1_c_int, x__len_ call f(tmp1_, out(tmp1_)) @@ -64,8 +64,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -153,9 +153,9 @@ extern void fn( - const double* const x__, - const double* const thresh__, - int* const out__, + const double* const x__, + const double* const thresh__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -230,7 +230,7 @@ ! manifest end - out = 0 + out = 0_c_int do tmp1_ = 1_c_int, x__len_ call closure1_(tmp1_, out(tmp1_)) @@ -258,8 +258,8 @@ extern void fn( - const double* const x__, - int* const out__, + const double* const x__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -347,9 +347,9 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { @@ -456,10 +456,10 @@ extern void fn( - const double* const x__, - const double* const thresh__, - int* const out__, - const R_len_t x__dim_1_, + const double* const x__, + const double* const thresh__, + int* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { @@ -579,10 +579,10 @@ extern void fn( - const double* const x__, - const int* const k__, - double* const out__, - const R_len_t x__dim_1_, + const double* const x__, + const int* const k__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { @@ -713,10 +713,10 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, - const R_len_t x__dim_2_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, + const R_len_t x__dim_2_, const R_len_t x__dim_3_); SEXP fn_(SEXP _args) { @@ -836,11 +836,11 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, - const R_len_t x__dim_2_, - const R_len_t x__dim_3_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, + const R_len_t x__dim_2_, + const R_len_t x__dim_3_, const R_len_t x__dim_4_); SEXP fn_(SEXP _args) { @@ -954,11 +954,11 @@ extern void fn( - const double* const x__, - const int* const k__, - double* const out__, - const R_len_t x__dim_1_, - const R_len_t x__dim_2_, + const double* const x__, + const int* const k__, + double* const out__, + const R_len_t x__dim_1_, + const R_len_t x__dim_2_, const R_len_t x__dim_3_); SEXP fn_(SEXP _args) { @@ -1050,7 +1050,7 @@ ! manifest end - out = 0 + out = 0.0_c_double do tmp1_ = 1_c_int, 12_c_int call closure1_(tmp1_, out(tmp1_)) @@ -1187,9 +1187,9 @@ extern void fn( - const double* const x__, - double* const out__, - const R_len_t x__dim_1_, + const double* const x__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/size-constraint.md b/tests/testthat/_snaps/size-constraint.md index 553b661..7c11e97 100644 --- a/tests/testthat/_snaps/size-constraint.md +++ b/tests/testthat/_snaps/size-constraint.md @@ -5,7 +5,7 @@ Output function(a, b) { declare(type(a = double(n)), type(b = double(n + 1))) - a <- sum(b) + a <- a + sum(b) a } @@ -26,7 +26,7 @@ ! manifest end - a = sum(b) + a = (a + sum(b)) end subroutine Code cat(cwrapper) @@ -37,8 +37,8 @@ extern void fn( - double* const a__, - const double* const b__, + double* const a__, + const double* const b__, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/subscript-validation.md b/tests/testthat/_snaps/subscript-validation.md index 9e50c2b..68563f1 100644 --- a/tests/testthat/_snaps/subscript-validation.md +++ b/tests/testthat/_snaps/subscript-validation.md @@ -37,9 +37,9 @@ extern void fn( - const double* const x__, - const int* const n__, - double* const out___, + const double* const x__, + const int* const n__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/superassignment-snapshots.md b/tests/testthat/_snaps/superassignment-snapshots.md index 150886b..e23b68d 100644 --- a/tests/testthat/_snaps/superassignment-snapshots.md +++ b/tests/testthat/_snaps/superassignment-snapshots.md @@ -64,8 +64,8 @@ extern void fn( - const int* const nx__, - const int* const ny__, + const int* const nx__, + const int* const ny__, double* const temp__); SEXP fn_(SEXP _args) { @@ -179,8 +179,8 @@ extern void fn( - double* const x__, - double* const out__, + double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -277,9 +277,9 @@ extern void fn( - const int* const nx__, - const int* const ny__, - const int* const nz__, + const int* const nx__, + const int* const ny__, + const int* const nz__, double* const a__); SEXP fn_(SEXP _args) { @@ -414,9 +414,9 @@ extern void fn( - double* const x__, - double* const out__, - const R_len_t x__dim_1_, + double* const x__, + double* const out__, + const R_len_t x__dim_1_, const R_len_t x__dim_2_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/svd.md b/tests/testthat/_snaps/svd.md index df02a59..93efafc 100644 --- a/tests/testthat/_snaps/svd.md +++ b/tests/testthat/_snaps/svd.md @@ -80,8 +80,8 @@ extern void fn( - const double* const x__, - double* const out___, + const double* const x__, + double* const out___, char* quickr_err_msg); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/type-promotion.md b/tests/testthat/_snaps/type-promotion.md index f7e25eb..8a81766 100644 --- a/tests/testthat/_snaps/type-promotion.md +++ b/tests/testthat/_snaps/type-promotion.md @@ -36,8 +36,8 @@ extern void fn( - const int* const x__, - double* const out___, + const int* const x__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -167,8 +167,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -231,8 +231,8 @@ extern void fn( - const int* const x__, - double* const out___, + const int* const x__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -293,8 +293,8 @@ extern void fn( - const int* const x__, - double* const out___, + const int* const x__, + double* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -356,9 +356,9 @@ extern void fn( - const int* const a__, - const double* const b__, - double* const out___, + const int* const a__, + const double* const b__, + double* const out___, const R_xlen_t a__len_); SEXP fn_(SEXP _args) { @@ -493,8 +493,8 @@ extern void fn( - const int* const a__, - const int* const b__, + const int* const a__, + const int* const b__, int* const out___); SEXP fn_(SEXP _args) { @@ -570,8 +570,8 @@ extern void fn( - const int* const x__, - int* const out___, + const int* const x__, + int* const out___, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/unary-intrinsics.md b/tests/testthat/_snaps/unary-intrinsics.md index 57e11fc..56aeb39 100644 --- a/tests/testthat/_snaps/unary-intrinsics.md +++ b/tests/testthat/_snaps/unary-intrinsics.md @@ -38,8 +38,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -102,8 +102,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -166,8 +166,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -230,8 +230,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -294,8 +294,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -358,8 +358,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -422,8 +422,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -486,8 +486,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -550,8 +550,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -614,8 +614,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -678,8 +678,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -742,8 +742,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -806,8 +806,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -870,8 +870,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -934,8 +934,8 @@ extern void fn( - const int* const x__, - int* const out__, + const int* const x__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -998,8 +998,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1062,8 +1062,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1126,8 +1126,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1190,8 +1190,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1254,8 +1254,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1318,8 +1318,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1382,8 +1382,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1446,8 +1446,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1510,8 +1510,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1574,8 +1574,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1638,8 +1638,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1702,8 +1702,8 @@ extern void fn( - const Rcomplex* const z__, - double* const out__, + const Rcomplex* const z__, + double* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1766,8 +1766,8 @@ extern void fn( - const Rcomplex* const z__, - double* const out__, + const Rcomplex* const z__, + double* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1830,8 +1830,8 @@ extern void fn( - const Rcomplex* const z__, - double* const out__, + const Rcomplex* const z__, + double* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1894,8 +1894,8 @@ extern void fn( - const Rcomplex* const z__, - double* const out__, + const Rcomplex* const z__, + double* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -1958,8 +1958,8 @@ extern void fn( - const Rcomplex* const z__, - Rcomplex* const out__, + const Rcomplex* const z__, + Rcomplex* const out__, const R_xlen_t z__len_); SEXP fn_(SEXP _args) { @@ -2021,8 +2021,8 @@ extern void fn( - const double* const x__, - double* const out__, + const double* const x__, + double* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { @@ -2091,8 +2091,8 @@ extern void fn( - const int* const x__, - int* const out__, + const int* const x__, + int* const out__, const R_xlen_t x__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/_snaps/which.md b/tests/testthat/_snaps/which.md index 62d0797..f9d32b6 100644 --- a/tests/testthat/_snaps/which.md +++ b/tests/testthat/_snaps/which.md @@ -170,12 +170,12 @@ extern void fn( - const int* const lgl1__, - const int* const int1__, - const double* const dbl1__, - int* const out__, - const R_xlen_t dbl1__len_, - const R_xlen_t int1__len_, + const int* const lgl1__, + const int* const int1__, + const double* const dbl1__, + int* const out__, + const R_xlen_t dbl1__len_, + const R_xlen_t int1__len_, const R_xlen_t lgl1__len_); SEXP fn_(SEXP _args) { diff --git a/tests/testthat/test-assignment-shape.R b/tests/testthat/test-assignment-shape.R new file mode 100644 index 0000000..3927e3b --- /dev/null +++ b/tests/testthat/test-assignment-shape.R @@ -0,0 +1,170 @@ +# Reassignment shape compatibility: quickr cannot re-declare a Fortran +# variable to R's new shape, so rank and every extent must stay +# compatible (the shape analogue of the narrowing check). + +skip_on_cran() + +test_that("reassignment to a statically different shape is a compile error", { + fn_vec <- function() { + x <- numeric(2) + x <- numeric(3) + x + } + expect_error( + quick(fn_vec), + "cannot reassign `x`: dimension 1 would change from 2 to 3", + fixed = TRUE + ) + + fn_mat <- function() { + x <- matrix(0, 2, 2) + x <- matrix(1, 2, 3) + x + } + expect_error( + quick(fn_mat), + "cannot reassign `x`: dimension 2 would change from 2 to 3", + fixed = TRUE + ) + + fn_c <- function() { + x <- c(1, 2) + x <- c(1, 2, 3) + x + } + expect_error( + quick(fn_c), + "cannot reassign `x`: dimension 1 would change from 2 to 3", + fixed = TRUE + ) + + fn_rank <- function(a) { + declare(type(a = double(2, 2))) + x <- c(1, 2) + x <- a + x + } + expect_error( + quick(fn_rank), + "cannot reassign `x`: replacement rank (2) differs from the declared rank (1)", + fixed = TRUE + ) + + fn_deferred_rank <- function(a) { + declare(type(a = double(2, 2)), type(x = double(NA))) + x <- a + 1 + } + expect_error( + quick(fn_deferred_rank), + "cannot reassign `x`: replacement rank (2) differs from the declared rank (1)", + fixed = TRUE + ) +}) + +test_that("reassignment with symbolic dims gets a runtime shape guard", { + fn <- function(a, b) { + declare(type(a = double(n)), type(b = double(m))) + x <- a + x <- b + x + } + qfn := quick(fn) + expect_identical(qfn(c(1, 2), c(3, 4)), c(3, 4)) + expect_error( + qfn(c(1, 2), c(3, 4, 5)), + "reassignment must preserve the shape of `x`" + ) +}) + +test_that("reassignment from a deferred-shape local gets a runtime guard", { + fn <- function(a, b) { + declare( + type(a = double(NA)), + type(b = double(NA)), + type(x = double(NA)) + ) + x <- b + a <- x + a + } + + code <- as.character(r2f(fn)) + expect_match( + code, + paste0( + "size\\(a, 1, kind=c_ptrdiff_t\\) /= ", + "size\\(x, 1, kind=c_ptrdiff_t\\)" + ) + ) + + qfn := quick(fn) + expect_identical(qfn(c(1, 2), c(3, 4)), c(3, 4)) + expect_error( + qfn(c(1, 2), c(3, 4, 5)), + "reassignment must preserve the shape of `a`" + ) +}) + +test_that("shape-preserving reassignments still compile", { + # same symbolic dims: provably equal, no guard needed + fn_same <- function(a) { + declare(type(a = double(n))) + x <- a + x <- a * 2 + x + } + expect_quick_identical(fn_same, c(1, 2, 3)) + + # two length-1 values conform whatever their ranks: a declared double(1) + # is rank 1, a literal is rank 0 + fn_len1 <- function(a) { + declare(type(a = double(1))) + a <- 2 + a + } + expect_quick_identical(fn_len1, 1) +}) + +test_that("reassignment between scalar and array shapes is refused", { + # R rebinds `x` to the scalar; Fortran would broadcast it across the + # array, so every element would change instead of the shape + fn_scalar_into_array <- function(a) { + declare(type(a = double(n))) + x <- a + x <- 0 + sum(x) + } + expect_error( + r2f(fn_scalar_into_array), + "replacement is a scalar but `x` is an array", + fixed = TRUE + ) + + # the reduction form of the same mistake + fn_reduce_into_array <- function(a) { + declare(type(a = double(n))) + x <- a + x <- sum(x) + x + } + expect_error( + r2f(fn_reduce_into_array), + "replacement is a scalar but `x` is an array", + fixed = TRUE + ) + + # the other direction: R rebinds to the array, Fortran would keep only + # the first element + fn_array_into_scalar <- function(a) { + declare(type(a = double(3))) + x <- 1 + x <- a + x + } + expect_error( + r2f(fn_array_into_scalar), + "replacement is an array but `x` is a scalar", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-bind.R b/tests/testthat/test-bind.R index a580aa2..cac2abb 100644 --- a/tests/testthat/test-bind.R +++ b/tests/testthat/test-bind.R @@ -104,7 +104,7 @@ test_that("cbind/rbind enforce common lengths", { test_that("cbind/rbind reject rank > 2 inputs with clear errors", { capture_bind_error <- function(expr) { - tryCatch(expr, error = function(e) cat(conditionMessage(e), "\n")) + tryCatch(expr, error = function(e) cat(conditionMessage(e), "\n", sep = "")) } bad_cbind <- function(x) { diff --git a/tests/testthat/test-blas-guards.R b/tests/testthat/test-blas-guards.R new file mode 100644 index 0000000..9ecfd27 --- /dev/null +++ b/tests/testthat/test-blas-guards.R @@ -0,0 +1,303 @@ +# Runtime conformability guards in BLAS/LAPACK lowerings: dims that cannot +# be verified at compile time get a size() check before the BLAS call +# (never a compile-time warning, never an unchecked call). + +skip_on_cran() + +test_that("matrix-vector %*% guards an unknown vector length", { + fn <- function(m, x) { + declare(type(m = double(3, 3)), type(x = double(NA))) + m %*% x + } + qfn <- expect_no_warning(quick(fn)) + expect_equal(qfn(diag(3), as.double(1:3)), diag(3) %*% 1:3) + # was: dgemv read past the end of x, returning garbage + expect_error( + qfn(diag(3), as.double(1:2)), + "non-conformable arguments in %*%", + fixed = TRUE + ) +}) + +test_that("vector-matrix %*% guards an unknown vector length", { + fn <- function(x, m) { + declare(type(x = double(NA)), type(m = double(3, 3))) + t(x) %*% m + } + qfn <- expect_no_warning(quick(fn)) + expect_equal(qfn(as.double(1:3), diag(3)), t(as.double(1:3)) %*% diag(3)) + expect_error( + qfn(as.double(1:5), diag(3)), + "non-conformable arguments in %*%", + fixed = TRUE + ) +}) + +test_that("triangular solve guards squareness and RHS length", { + fn <- function(l, x) { + declare(type(l = double(n, k)), type(x = double(NA))) + forwardsolve(l, x) + } + qfn <- expect_no_warning(quick(fn)) + l <- matrix(c(1, 2, 0, 3), 2, 2) + expect_equal(qfn(l, c(1, 5)), forwardsolve(l, c(1, 5))) + expect_error( + qfn(l, c(1, 5, 9)), + "non-conformable arguments in triangular solve" + ) + expect_error( + qfn(matrix(as.double(1:6), 2, 3), c(1, 5)), + "triangular solve requires a square matrix" + ) +}) + +test_that("vector %*% vector guards unknown lengths as whole sizes", { + fn <- function(x, y) { + declare(type(x = double(NA)), type(y = double(NA))) + x %*% y + } + # was: the fallthrough guard hardcoded rank-2 axes, emitting size(x, 2) + # on a rank-1 array -- a gfortran error that made a conformable + # unknown-length dot product fail to compile at all + qfn <- expect_no_warning(quick(fn)) + expect_equal(qfn(c(1, 2, 3), c(4, 5, 6)), c(1, 2, 3) %*% c(4, 5, 6)) + expect_error( + qfn(c(1, 2), c(4, 5, 6)), + "non-conformable arguments in %*%", + fixed = TRUE + ) +}) + +test_that("solve() guards an unknown RHS length", { + fn <- function(a, b) { + declare(type(a = double(2, 2)), type(b = double(NA))) + solve(a, b) + } + qfn <- expect_no_warning(quick(fn)) + expect_equal(qfn(diag(2), c(1, 2)), c(1, 2)) + expect_error(qfn(diag(2), c(1, 2, 3)), "non-conformable arguments in solve") +}) + +test_that("solve() rejects a zero-width matrix right-hand side", { + known <- function(a, b) { + declare(type(a = double(2, 2)), type(b = double(2, 0))) + solve(a, b) + } + dynamic <- function(a, b) { + declare(type(a = double(2, 2)), type(b = double(2, NA))) + solve(a, b) + } + a <- diag(2) + b <- matrix(double(), 2, 0) + + expect_error(quick(known), "no right-hand side in 'b'", fixed = TRUE) + q_dynamic <- expect_no_warning(quick(dynamic)) + expect_error(q_dynamic(a, b), "no right-hand side in 'b'", fixed = TRUE) +}) + + +test_that("solve(a) and chol() guard squareness", { + inv <- function(a) { + declare(type(a = double(n, k))) + solve(a) + } + qinv <- expect_no_warning(quick(inv)) + expect_equal(qinv(diag(2)), diag(2)) + expect_error( + qinv(matrix(as.double(1:6), 2, 3)), + "solve requires a square matrix" + ) + + chol_fn <- function(a) { + declare(type(a = double(n, k))) + chol(a) + } + qchol <- expect_no_warning(quick(chol_fn)) + expect_equal(qchol(diag(2)), diag(2)) + expect_error( + qchol(matrix(as.double(1:6), 2, 3)), + "chol requires a square matrix" + ) +}) + +test_that("matrix-matrix %*% returns zeros for a known empty contraction", { + fn <- function(a, b) { + declare(type(a = double(2, 0)), type(b = double(0, 3))) + a %*% b + } + expect_quick_identical( + fn, + list(matrix(double(), 2, 0), matrix(double(), 0, 3)) + ) +}) + +test_that("matrix-vector %*% returns zeros for a known empty contraction", { + fn <- function(a, x) { + declare(type(a = double(2, 0)), type(x = double(0))) + a %*% x + } + expect_quick_identical(fn, list(matrix(double(), 2, 0), double())) +}) + +test_that("%*% returns zeros for a symbolic empty contracted dimension", { + fn <- function(a, b, k) { + declare( + type(a = double(2, k)), + type(b = double(k, 3)), + type(k = integer(1)) + ) + a %*% b + } + expect_quick_identical( + fn, + list(matrix(double(), 2, 0), matrix(double(), 0, 3), 0L) + ) +}) + +test_that("symmetric products return zeros for known empty contractions", { + cross_vec <- function(x) { + declare(type(x = double(0))) + crossprod(x) + } + cross_mat <- function(x) { + declare(type(x = double(0, 2))) + crossprod(x) + } + tcross_mat <- function(x) { + declare(type(x = double(2, 0))) + tcrossprod(x) + } + cross_symbolic <- function(x, n) { + declare(type(x = double(n, 2)), type(n = integer(1))) + crossprod(x) + } + tcross_symbolic <- function(x, n) { + declare(type(x = double(2, n)), type(n = integer(1))) + tcrossprod(x) + } + + expect_quick_identical(cross_vec, list(double())) + expect_quick_identical(cross_mat, list(matrix(double(), 0, 2))) + expect_quick_identical(tcross_mat, list(matrix(double(), 2, 0))) + expect_quick_identical(cross_symbolic, list(matrix(double(), 0, 2), 0L)) + expect_quick_identical(tcross_symbolic, list(matrix(double(), 2, 0), 0L)) +}) + +test_that("matrix BLAS rejects known zero-sized outputs", { + matrix_matrix <- function(a, b) { + declare(type(a = double(0, 2)), type(b = double(2, 3))) + a %*% b + } + matrix_vector <- function(a, x) { + declare(type(a = double(0, 2)), type(x = double(2))) + a %*% x + } + tcross_vec <- function(x) { + declare(type(x = double(0))) + tcrossprod(x) + } + cross_mat <- function(x) { + declare(type(x = double(0, 0))) + crossprod(x) + } + + expect_error(quick(matrix_matrix), "zero-sized outputs are not supported") + expect_error(quick(matrix_vector), "zero-sized outputs are not supported") + expect_error(quick(tcross_vec), "zero-sized outputs are not supported") + expect_error(quick(cross_mat), "zero-sized outputs are not supported") +}) + +test_that("matrix BLAS guards unknown output extents at runtime", { + matrix_matrix <- function(a, b) { + declare(type(a = double(NA, 2)), type(b = double(2, 3))) + a %*% b + } + matrix_vector <- function(a, x) { + declare(type(a = double(NA, 2)), type(x = double(2))) + a %*% x + } + cross_mat <- function(x) { + declare(type(x = double(2, NA))) + crossprod(x) + } + tcross_mat <- function(x) { + declare(type(x = double(NA, 2))) + tcrossprod(x) + } + + q_matrix_matrix <- expect_no_warning(quick(matrix_matrix)) + q_matrix_vector <- expect_no_warning(quick(matrix_vector)) + q_cross_mat <- expect_no_warning(quick(cross_mat)) + q_tcross_mat <- expect_no_warning(quick(tcross_mat)) + message <- "zero-sized outputs are not supported" + + expect_error( + q_matrix_matrix(matrix(double(), 0, 2), matrix(double(), 2, 3)), + message + ) + expect_error(q_matrix_vector(matrix(double(), 0, 2), double(2)), message) + expect_error(q_cross_mat(matrix(double(), 2, 0)), message) + expect_error(q_tcross_mat(matrix(double(), 0, 2)), message) +}) + +test_that("outer products reject known zero-sized outputs", { + outer_empty_x <- function(x, y) { + declare(type(x = double(0)), type(y = double(2))) + outer(x, y) + } + percent_outer_empty_y <- function(x, y) { + declare(type(x = double(2)), type(y = double(0))) + x %o% y + } + + expect_error( + quick(outer_empty_x), + "outer zero-sized outputs are not supported", + fixed = TRUE + ) + expect_error( + quick(percent_outer_empty_y), + "%o% zero-sized outputs are not supported", + fixed = TRUE + ) +}) + +test_that("outer products guard unknown output extents at runtime", { + outer_unknown_x <- function(x, y) { + declare(type(x = double(NA)), type(y = double(2))) + outer(x, y) + } + percent_outer_unknown_y <- function(x, y) { + declare(type(x = double(2)), type(y = double(NA))) + x %o% y + } + + q_outer_unknown_x <- expect_no_warning(quick(outer_unknown_x)) + q_percent_outer_unknown_y <- expect_no_warning(quick(percent_outer_unknown_y)) + expect_error( + q_outer_unknown_x(double(), as.double(1:2)), + "outer zero-sized outputs are not supported", + fixed = TRUE + ) + expect_error( + q_percent_outer_unknown_y(as.double(1:2), double()), + "%o% zero-sized outputs are not supported", + fixed = TRUE + ) +}) + +test_that("NA dims are never treated as equal", { + fn <- function(a, b) { + declare(type(a = double(NA, NA)), type(b = double(NA, NA))) + a %*% b + } + qfn <- expect_no_warning(quick(fn)) + m <- matrix(as.double(1:4), 2, 2) + expect_equal(qfn(m, m), m %*% m) + # was: identical(NA, NA) blessed the pair with no check at all + expect_error( + qfn(m, matrix(as.double(1:6), 3, 2)), + "non-conformable arguments in %*%", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-c-bridge-hoist.R b/tests/testthat/test-c-bridge-hoist.R index 94a5c22..a77b47c 100644 --- a/tests/testthat/test-c-bridge-hoist.R +++ b/tests/testthat/test-c-bridge-hoist.R @@ -25,3 +25,34 @@ test_that("size check blocks redeclare hoisted size temps", { list(5L, 3L, c(1, 2, 3), c(4, 5, 6)) ) }) + +test_that("C bridge casts completed real size expressions", { + fn <- function(x, y) { + declare(type(x = double(1)), type(y = double(1))) + out <- diag(x + y) + out + } + + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn := quick(fn) + + dll_paths <- setdiff(loaded_dll_paths(), dll_paths_before) + expect_length(dll_paths, 1L) + c_path <- list.files( + dirname(dll_paths[[1L]]), + pattern = "_c_wrapper[.]c$", + full.names = TRUE + ) + expect_length(c_path, 1L) + c_code <- paste(readLines(c_path, warn = FALSE), collapse = "\n") + expect_match( + c_code, + "((R_xlen_t)((Rf_asReal(x) + Rf_asReal(y))))", + fixed = TRUE + ) + + x <- c(1.7) + y <- c(1.7) + expect_identical(qfn(x, y), fn(x, y)) +}) diff --git a/tests/testthat/test-classes.R b/tests/testthat/test-classes.R index b2ab3f5..b038fb1 100644 --- a/tests/testthat/test-classes.R +++ b/tests/testthat/test-classes.R @@ -2,15 +2,14 @@ skip_on_cran() -test_that("prop helpers implement coercion, validation and set-once behavior", { +test_that("prop helpers implement coercion and validation", { Test <- S7::new_class( name = "Test", properties = list( s = quickr:::prop_string( default = NULL, allow_null = TRUE, - coerce = TRUE, - set_once = TRUE + coerce = TRUE ), n = quickr:::prop_wholenumber( default = 1, @@ -29,7 +28,6 @@ test_that("prop helpers implement coercion, validation and set-once behavior", { obj@s <- 123 expect_identical(obj@s, "123") - expect_error(obj@s <- "again", "can only be set once") obj@n <- 2 expect_identical(obj@n, 2L) @@ -77,13 +75,13 @@ test_that("Fortran validates length and prints non-null properties", { expect_true(any(grepl("@r:", out, fixed = TRUE))) }) -test_that("new_setter returns NULL when no coercion or set_once", { - result <- quickr:::new_setter(coerce = FALSE, set_once = FALSE) +test_that("new_setter returns NULL when no coercion", { + result <- quickr:::new_setter(coerce = FALSE) expect_null(result) }) test_that("new_setter handles coerce = NULL correctly", { - result <- quickr:::new_setter(coerce = NULL, set_once = FALSE) + result <- quickr:::new_setter(coerce = NULL) expect_null(result) }) diff --git a/tests/testthat/test-codecov-coverage.R b/tests/testthat/test-codecov-coverage.R index 14c0f06..f55b8bf 100644 --- a/tests/testthat/test-codecov-coverage.R +++ b/tests/testthat/test-codecov-coverage.R @@ -883,7 +883,7 @@ test_that("elementwise vector lengths must recycle cleanly", { expect_error( quick(fn), - "elementwise vector operations require lengths that recycle cleanly", + "elementwise vector operations require equal lengths", fixed = TRUE ) }) diff --git a/tests/testthat/test-compiler.R b/tests/testthat/test-compiler.R index d4862c0..e5ca142 100644 --- a/tests/testthat/test-compiler.R +++ b/tests/testthat/test-compiler.R @@ -31,6 +31,17 @@ local_empty_compiler_probe_cache <- function(envir = parent.frame()) { invisible(NULL) } +local_flang_auto_enabled <- function(envir = parent.frame()) { + state <- quickr:::quickr_flang_state + old_auto_disabled <- state$auto_disabled + withr::defer( + state$auto_disabled <- old_auto_disabled, + envir = envir + ) + state$auto_disabled <- FALSE + invisible(NULL) +} + test_that("R CMD config probe distinguishes empty values from errors", { probe_value <- character() local_mocked_bindings( @@ -537,6 +548,7 @@ test_that("quick retries failed R CMD config probes", { test_that("quick caches successful flang probes by resolved path", { local_empty_compiler_probe_cache() + local_flang_auto_enabled() withr::local_options(quickr.fortran_compiler = "auto") flang <- file.path(tempdir(), "flang-one") @@ -588,6 +600,7 @@ test_that("quick caches successful flang probes by resolved path", { test_that("quick retries failed flang probes", { local_empty_compiler_probe_cache() + local_flang_auto_enabled() withr::local_options(quickr.fortran_compiler = "auto") flang <- file.path(tempdir(), "flang") diff --git a/tests/testthat/test-conformability-grid.R b/tests/testthat/test-conformability-grid.R new file mode 100644 index 0000000..0404337 --- /dev/null +++ b/tests/testthat/test-conformability-grid.R @@ -0,0 +1,836 @@ +# Combinatorial enforcement of the conformability contract: every cell of +# mode(left) x mode(right) x shape(left) x shape(right) x op +# is checked against plain R as the oracle. + +skip_on_cran() + +# Valid cells must match R exactly +# (values, typeof(), shape); statically invalid cells must fail to compile +# with the documented message; statically undecidable cells must compile a +# runtime guard that matches R on conformable inputs and raises the +# documented error on nonconformable ones. +# +# Compile-cost control: cells sharing a shape pair are packed into one +# compiled function with one statement per (op, mode pair, operand order), +# so one gfortran invocation covers up to ~90 cells. Ops split into two +# packed families because both operand orders appear in one function, so +# every operand is also a divisor/exponent somewhere: +# gen: + - * < == & | -- safe for any values (zeros, FALSE, negatives) +# div: / ^ %% %/% -- operands chosen zero-free and pow-safe, since +# R answers the unsafe cells with NA/NaN (documented not-supported) +# or traps SIGFPE in Fortran integer division +# Value edges ride along as input choices: negative dividends/divisors and +# bases (%% sign semantics, integer-exponent ^), magnitudes past 2^24 and +# 2^31 (%/% in the real domain), descending ranges, TRUE/FALSE arithmetic, +# and equal positions so == has TRUE cells. +# +# Every intended shape pair runs in the standard non-CRAN suite. + +# --- Axes --------------------------------------------------------------- + +grid_modes <- c(l = "logical", i = "integer", d = "double") + +grid_shapes <- list( + scl = list(decl = "1", kind = "scalar", n = 1L), + vec3 = list(decl = "3", kind = "vec", len = 3L, n = 3L), + vec4 = list(decl = "4", kind = "vec", len = 4L, n = 4L), + mat32 = list(decl = "3, 2", kind = "mat", dims = c(3L, 2L), n = 6L), + mat11 = list(decl = "1, 1", kind = "mat", dims = c(1L, 1L), n = 1L), + sym = list(decl = "NA", kind = "vec", len = NA_integer_, n = 3L) +) + +grid_op_families <- list( + gen = c( + add = "+", + sub = "-", + mul = "*", + lt = "<", + eq = "==", + and = "&", + or = "|" + ), + div = c(div = "/", pow = "^", mod = "%%", idv = "%/%") +) + +# quickr requires logical operands for & and | (R would coerce numerics); +# an error divergence, pinned in its own test below. +grid_logical_only_ops <- c("and", "or") + +grid_mode_pairs <- function(opname) { + if (opname %in% grid_logical_only_ops) { + return(list(c("l", "l"))) + } + pairs <- expand.grid( + names(grid_modes), + names(grid_modes), + stringsAsFactors = FALSE + ) + lapply(seq_len(nrow(pairs)), function(i) c(pairs[i, 2L], pairs[i, 1L])) +} + +# --- Values ------------------------------------------------------------- + +# Six values per (family, set, role, mode); shapes take a prefix (scalars +# and 1x1 matrices position 1, vec3/sym positions 1:3, mat32 all six). +grid_value_pool <- list( + gen = list( + primary = list( + a = list( + l = c(TRUE, FALSE, TRUE, FALSE, TRUE, FALSE), + i = c(-5L, 3L, 0L, 9L, 2L, -4L), + d = c(1.5, 0, -100, 8.25, -2, 0.125) + ), + b = list( + l = c(TRUE, TRUE, FALSE, FALSE, TRUE, FALSE), + i = c(2L, -3L, 4L, 0L, 2L, -7L), + d = c(1.5, -2, 3, 0, -2, 8) + ) + ), + # extremes (products stay inside int32), descending runs, equal positions + edge = list( + a = list( + l = c(FALSE, TRUE, TRUE, FALSE, FALSE, TRUE), + i = c(40000L, -40000L, 6L, 5L, 4L, 3L), + d = c(1e10, -1e10, 2.5, 2.5, -0.5, 0) + ), + b = list( + l = c(FALSE, FALSE, TRUE, TRUE, FALSE, TRUE), + i = c(40000L, 40000L, -6L, 5L, -4L, 0L), + d = c(1e10, 1e10, -2.5, 2.5, 0.5, -8) + ) + ) + ), + div = list( + # No zeros anywhere (either operand may be a divisor); doubles paired so + # a negative base only ever meets a whole-valued exponent (R answers the + # fractional case NaN, which is out of scope). + primary = list( + a = list( + l = rep(TRUE, 6L), + i = c(-5L, 3L, 7L, 9L, 2L, -4L), + d = c(1.5, 3, 100, 8.25, 2, 2) + ), + b = list( + l = rep(TRUE, 6L), + i = c(2L, -3L, 4L, 3L, 2L, -7L), + d = c(2, 3, 2, 4, 2, 3) + ) + ), + # 1e10: %/% quotient past both 2^24 and 2^31, exact in doubles, so the + # old FLOOR()-to-int32 overflow would surface without float noise. + edge = list( + a = list( + l = rep(TRUE, 6L), + i = c(9L, 7L, 5L, -3L, 2L, 12L), + d = c(1e10, -7.25, 4, 0.25, 9.5, 2) + ), + b = list( + l = rep(TRUE, 6L), + i = c(1L, 2L, -2L, 4L, 2L, 1L), + d = c(1, 2, 2, 4, 3, 2) + ) + ) + ) +) + +grid_operand <- function(role, family, set, mode, shape, sym_len = 3L) { + pool <- grid_value_pool[[family]][[set]][[role]][[mode]] + s <- grid_shapes[[shape]] + n <- if (identical(shape, "sym")) sym_len else s$n + x <- rep_len(pool, n) + if (s$kind == "mat") { + x <- matrix(x, nrow = s$dims[1L], ncol = s$dims[2L]) + } + x +} + +grid_pair_args <- function( + sa, + sb, + family, + set, + sym_len_a = 3L, + sym_len_b = 3L +) { + args <- list() + for (m in names(grid_modes)) { + args[[paste0("a", m)]] <- grid_operand("a", family, set, m, sa, sym_len_a) + args[[paste0("b", m)]] <- grid_operand("b", family, set, m, sb, sym_len_b) + } + args +} + +# Length a `sym` operand must have to conform with its partner shape. +# For a matrix partner that is the vector-matrix rule's nrow -- including +# the 1x1 matrix, whose symbolic-vector cells guard on length 1. +grid_sym_ok_len <- function(partner) { + p <- grid_shapes[[partner]] + if (p$kind == "vec" && !is.na(p$len)) { + p$len + } else if (p$kind == "mat") { + p$dims[1L] + } else { + 3L + } +} + +# --- Generators --------------------------------------------------------- + +# One function per (shape pair, op family): one statement per +# (op, mode pair, operand order), returning every result in a named list. +make_grid_pair_fn <- function(sa, sb, family) { + decls <- c( + vapply( + names(grid_modes), + function(m) { + paste0( + "type(a", + m, + " = ", + grid_modes[[m]], + "(", + grid_shapes[[sa]]$decl, + "))" + ) + }, + "" + ), + vapply( + names(grid_modes), + function(m) { + paste0( + "type(b", + m, + " = ", + grid_modes[[m]], + "(", + grid_shapes[[sb]]$decl, + "))" + ) + }, + "" + ) + ) + ids <- character() + stmts <- character() + ops <- grid_op_families[[family]] + pair_verdict <- grid_pair_verdict(sa, sb) + for (opname in names(ops)) { + if (!identical(grid_cell_verdict(sa, sb, opname), pair_verdict)) { + next # strict-op 1x1 rows: covered by the compile-error sweep + } + for (p in grid_mode_pairs(opname)) { + for (ord in c("ab", "ba")) { + id <- paste0("r_", ord, "_", opname, "_", p[1L], p[2L]) + expr <- if (identical(ord, "ab")) { + paste0("a", p[1L], " ", ops[[opname]], " b", p[2L]) + } else { + paste0("b", p[2L], " ", ops[[opname]], " a", p[1L]) + } + ids <- c(ids, id) + stmts <- c(stmts, paste0(" ", id, " <- ", expr)) + } + } + } + src <- paste0( + "function(al, ai, ad, bl, bi, bd) {\n", + " declare(\n ", + paste(decls, collapse = ",\n "), + "\n )\n", + paste(stmts, collapse = "\n"), + "\n list(\n ", + paste(paste0(ids, " = ", ids), collapse = ",\n "), + "\n )\n}" + ) + eval(parse(text = src)[[1L]]) +} + +# A one-cell function, for cells whose expected outcome is a compile error. +make_grid_cell_fn <- function(sa, sb, op, ma, mb) { + src <- paste0( + "function(a, b) {\n", + " declare(type(a = ", + grid_modes[[ma]], + "(", + grid_shapes[[sa]]$decl, + ")), ", + "type(b = ", + grid_modes[[mb]], + "(", + grid_shapes[[sb]]$decl, + ")))\n", + " a ", + op, + " b\n", + "}" + ) + eval(parse(text = src)[[1L]]) +} + +# --- The expected-outcome function -------------------------------------- +# Direct transcription of the shape-contract table, per cell: +# 1. scalar op anything -> allow, no guard +# 2. identical known shapes -> allow, no guard +# 3. vec(n) op mat(n, k) -> allow (column-major recycling) +# 4. known mismatch (incl. length 0) -> compile error +# 5. not statically decidable (NA dims) -> runtime guard +# One op-class split, mirroring R: *arithmetic* recycles a 1x1 matrix +# against a vector of statically known length != 1 (deprecated in R but +# still its answer, so quickr scalarizes), while comparisons and & | error +# there -- for those the 1x1 is an ordinary one-row matrix and the +# vector-matrix rule applies. A *symbolic* vector length takes the +# vector-matrix rule for every op class: the result's shape depends on the +# runtime length (R keeps the 1x1 dims only for a length-1 vector), so a +# runtime guard requires length 1 and longer vectors error where R would +# recycle (both flavors are pinned in test-recycling.R). + +grid_strict_ops <- c("lt", "eq", "and", "or") + +grid_cell_verdict <- function(sa, sb, opname) { + A <- grid_shapes[[sa]] + B <- grid_shapes[[sb]] + is_1x1 <- function(s) s$kind == "mat" && all(s$dims == 1L) + ok <- list(outcome = "ok") + guard <- function(msg) list(outcome = "guard", guard_msg = msg) + err <- function(msg) list(outcome = "error", msg = msg) + + if (A$kind == "scalar" || B$kind == "scalar") { + return(ok) + } + if ((is_1x1(A) && B$kind == "vec") || (is_1x1(B) && A$kind == "vec")) { + vec <- if (A$kind == "vec") A else B + if (is.na(vec$len)) { + return(guard("matrix first dimension")) + } + if (vec$len == 1L) { + return(ok) + } + if (opname %in% grid_strict_ops) { + return(err("matrix first dimension")) + } + return(ok) # scalarized 1x1: R's length-1 array recycling + } + if (A$kind == "vec" && B$kind == "vec") { + if (is.na(A$len) || is.na(B$len)) { + return(guard("equal lengths")) + } + if (A$len == B$len) { + return(ok) + } + return(err("equal lengths")) + } + if (A$kind == "mat" && B$kind == "mat") { + if (all(A$dims == B$dims)) { + return(ok) + } + return(err("matching dimensions")) + } + # vector op matrix: vector length against nrow + vec <- if (A$kind == "vec") A else B + mat <- if (A$kind == "mat") A else B + if (is.na(vec$len)) { + return(guard("matrix first dimension")) + } + if (vec$len == mat$dims[1L]) { + return(ok) + } + err("matrix first dimension") +} + +# Shape-pair verdict for packing: the arithmetic-class verdict. Cells whose +# own verdict differs (the strict-op 1x1 rows) are excluded from the packed +# function and land in the compile-error sweep instead. +grid_pair_verdict <- function(sa, sb) { + grid_cell_verdict(sa, sb, "add") +} + +# --- Oracle comparison -------------------------------------------------- + +# suppressWarnings: R deprecation-warns on 1x1-array-vs-vector recycling +# (mat11 pairs); values still match, which is what the contract pins. +expect_grid_cells_match <- function(qfn, fn, args, context) { + r_res <- suppressWarnings(do.call(fn, args)) + q_res <- do.call(qfn, args) + expect_identical(names(q_res), names(r_res)) + for (nm in names(r_res)) { + expect_equal( + q_res[[nm]], + r_res[[nm]], + label = paste0(context, " ", nm, " (quickr)"), + expected.label = "R" + ) + expect_identical( + typeof(q_res[[nm]]), + typeof(r_res[[nm]]), + label = paste0(context, " ", nm, " typeof (quickr)"), + expected.label = "typeof (R)" + ) + } +} + +# --- Elementwise grid: valid and guarded shape pairs --------------------- + +grid_pair_names <- names(grid_shapes) +for (i in seq_along(grid_pair_names)) { + for (j in seq.int(i, length(grid_pair_names))) { + local({ + sa <- grid_pair_names[[i]] + sb <- grid_pair_names[[j]] + verdict <- grid_pair_verdict(sa, sb) + if (identical(verdict$outcome, "error")) { + return() # handled in the compile-error section below + } + pair_id <- paste0(sa, ".", sb) + + for (family in names(grid_op_families)) { + test_that(paste0("elementwise grid ", pair_id, " [", family, "]"), { + fn <- make_grid_pair_fn(sa, sb, family) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + + sym_a <- if (identical(sa, "sym")) grid_sym_ok_len(sb) else 3L + sym_b <- if (identical(sb, "sym")) grid_sym_ok_len(sa) else 3L + for (set in c("primary", "edge")) { + args <- grid_pair_args(sa, sb, family, set, sym_a, sym_b) + expect_grid_cells_match( + qfn, + fn, + args, + context = paste0(pair_id, "/", set) + ) + } + + if (identical(verdict$outcome, "guard")) { + # bump one symbolic operand's length; the guard must raise the + # documented error where R errors and BLAS-free Fortran would + # read or write out of bounds + bad_b <- if (identical(sb, "sym")) sym_b + 1L else sym_b + bad_a <- if (identical(sb, "sym")) sym_a else sym_a + 1L + args_bad <- grid_pair_args(sa, sb, family, "primary", bad_a, bad_b) + expect_error( + do.call(qfn, args_bad), + verdict$guard_msg, + fixed = TRUE + ) + } + }) + } + }) + } +} + +# --- Elementwise grid: statically rejected shape pairs ------------------- +# Compile errors never reach gfortran, so every op and both operand orders +# are cheap enough to always run. + +test_that("statically nonconformable cells are compile errors for every op", { + for (i in seq_along(grid_pair_names)) { + for (j in seq.int(i, length(grid_pair_names))) { + sa <- grid_pair_names[[i]] + sb <- grid_pair_names[[j]] + for (family in names(grid_op_families)) { + ops <- grid_op_families[[family]] + for (opname in names(ops)) { + verdict <- grid_cell_verdict(sa, sb, opname) + if (!identical(verdict$outcome, "error")) { + next + } + modes <- if (opname %in% grid_logical_only_ops) { + c("l", "l") + } else { + c("d", "d") + } + for (ord in list(c(sa, sb), c(sb, sa))) { + fn <- make_grid_cell_fn( + ord[1L], + ord[2L], + ops[[opname]], + modes[1L], + modes[2L] + ) + expect_error( + quick(fn), + verdict$msg, + fixed = TRUE, + label = paste0( + "quick() for ", + ord[1L], + " ", + ops[[opname]], + " ", + ord[2L] + ) + ) + } + } + } + } + } +}) + +test_that("known length-0 operands are compile errors", { + fn <- eval(parse( + text = paste0( + "function(a, b) {\n", + " declare(type(a = double(0)), type(b = double(4)))\n", + " a + b\n}" + ) + )[[1L]]) + expect_error(quick(fn), "equal lengths", fixed = TRUE) +}) + +test_that("& and | require logical operands (R would coerce: error divergence)", { + for (op in c("&", "|")) { + for (ma in names(grid_modes)) { + for (mb in names(grid_modes)) { + if (identical(ma, "l") && identical(mb, "l")) { + next + } + fn <- make_grid_cell_fn("vec3", "vec3", op, ma, mb) + expect_error( + quick(fn), + "requires logical operands", + fixed = TRUE, + label = paste0( + "quick() for ", + grid_modes[[ma]], + " ", + op, + " ", + grid_modes[[mb]] + ) + ) + } + } + } +}) + +# --- c(): mode join over all elements, constructive lengths -------------- + +test_that("c() grid: lattice join across modes, known and mixed lengths", { + ids <- character() + stmts <- character() + for (ma in names(grid_modes)) { + for (mb in names(grid_modes)) { + id_ab <- paste0("r_c_", ma, mb) + id_sb <- paste0("r_cs_", ma, mb) + ids <- c(ids, id_ab, id_sb) + stmts <- c( + stmts, + paste0(" ", id_ab, " <- c(a", ma, ", b", mb, ")"), + paste0(" ", id_sb, " <- c(s", ma, ", b", mb, ")") + ) + } + } + ids <- c(ids, "r_c3") + stmts <- c(stmts, " r_c3 <- c(sl, ai, bd)") # three-mode join + src <- paste0( + "function(al, ai, ad, bl, bi, bd, sl, si, sd) {\n", + " declare(\n", + " type(al = logical(3)), type(ai = integer(3)), type(ad = double(3)),\n", + " type(bl = logical(4)), type(bi = integer(4)), type(bd = double(4)),\n", + " type(sl = logical(1)), type(si = integer(1)), type(sd = double(1))\n", + " )\n", + paste(stmts, collapse = "\n"), + "\n list(\n ", + paste(paste0(ids, " = ", ids), collapse = ",\n "), + "\n )\n}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + for (set in c("primary", "edge")) { + args <- c( + grid_pair_args("vec3", "vec4", "gen", set), + list( + sl = grid_operand("a", "gen", set, "l", "scl"), + si = grid_operand("a", "gen", set, "i", "scl"), + sd = grid_operand("a", "gen", set, "d", "scl") + ) + ) + expect_grid_cells_match(qfn, fn, args, context = paste0("c()/", set)) + } +}) + +test_that("c() grid: symbolic lengths are constructive (no guard)", { + src <- paste0( + "function(al, ai, ad, bl, bi, bd) {\n", + " declare(\n", + " type(al = logical(NA)), type(ai = integer(NA)), type(ad = double(NA)),\n", + " type(bl = logical(NA)), type(bi = integer(NA)), type(bd = double(NA))\n", + " )\n", + " r_ll <- c(al, bl)\n", + " r_id <- c(ai, bd)\n", + " r_dl <- c(ad, bl)\n", + " r_dd <- c(ad, bd)\n", + " list(r_ll = r_ll, r_id = r_id, r_dl = r_dl, r_dd = r_dd)\n", + "}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + # unequal lengths are fine for c(): lengths add, nothing to conform + args <- grid_pair_args( + "sym", + "sym", + "gen", + "primary", + sym_len_a = 3L, + sym_len_b = 4L + ) + expect_grid_cells_match(qfn, fn, args, context = "c()/sym") +}) + +test_that("c() flattens rank-2 args column-major, like R", { + fn <- eval(parse( + text = paste0( + "function(a) {\n", + " declare(type(a = double(3, 2)))\n", + " c(a, 1.0)\n}" + ) + )[[1L]]) + expect_quick_identical(fn, list(matrix(as.double(1:6), 3, 2))) +}) + +# --- Multi-arg min()/max()/sum(): join across args, shapes independent ---- + +test_that("multi-arg min/max/sum grid: modes join, arg shapes independent", { + ids <- character() + stmts <- character() + for (fname in c("min", "max", "sum")) { + for (ma in names(grid_modes)) { + for (mb in names(grid_modes)) { + id <- paste0("r_", fname, "_", ma, mb) + ids <- c(ids, id) + stmts <- c( + stmts, + paste0(" ", id, " <- ", fname, "(a", ma, ", b", mb, ")") + ) + } + } + } + ids <- c(ids, "r_min3", "r_max3") + stmts <- c( + stmts, + " r_min3 <- min(al, bi, sd)", + " r_max3 <- max(al, bi, sd)" + ) + src <- paste0( + "function(al, ai, ad, bl, bi, bd, sd) {\n", + " declare(\n", + " type(al = logical(3)), type(ai = integer(3)), type(ad = double(3)),\n", + " type(bl = logical(4)), type(bi = integer(4)), type(bd = double(4)),\n", + " type(sd = double(1))\n", + " )\n", + paste(stmts, collapse = "\n"), + "\n list(\n ", + paste(paste0(ids, " = ", ids), collapse = ",\n "), + "\n )\n}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + for (set in c("primary", "edge")) { + args <- c( + grid_pair_args("vec3", "vec4", "gen", set), + list(sd = grid_operand("a", "gen", set, "d", "scl")) + ) + expect_grid_cells_match( + qfn, + fn, + args, + context = paste0("min/max/sum/", set) + ) + } +}) + +# --- ifelse(): branch-mode join, shape from `test` ------------------------ + +test_that("ifelse grid: branch mode pairs join; scalars broadcast against vector test", { + ids <- character() + stmts <- character() + for (my in names(grid_modes)) { + for (mn in names(grid_modes)) { + for (combo in c("vv", "sv", "vs", "ss")) { + id <- paste0("r_", combo, "_", my, mn) + yes <- if (substr(combo, 1L, 1L) == "v") { + paste0("y", my) + } else { + paste0("p", my) + } + no <- if (substr(combo, 2L, 2L) == "v") { + paste0("n", mn) + } else { + paste0("q", mn) + } + ids <- c(ids, id) + stmts <- c( + stmts, + paste0(" ", id, " <- ifelse(t3, ", yes, ", ", no, ")") + ) + } + } + } + src <- paste0( + "function(t3, yl, yi, yd, nl, ni, nd, pl, pi, pd, ql, qi, qd) {\n", + " declare(\n", + " type(t3 = logical(3)),\n", + " type(yl = logical(3)), type(yi = integer(3)), type(yd = double(3)),\n", + " type(nl = logical(3)), type(ni = integer(3)), type(nd = double(3)),\n", + " type(pl = logical(1)), type(pi = integer(1)), type(pd = double(1)),\n", + " type(ql = logical(1)), type(qi = integer(1)), type(qd = double(1))\n", + " )\n", + paste(stmts, collapse = "\n"), + "\n list(\n ", + paste(paste0(ids, " = ", ids), collapse = ",\n "), + "\n )\n}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + for (set in c("primary", "edge")) { + # `test` must stay mixed TRUE/FALSE: with a one-sided test R's ifelse + # never materializes the untaken branch, so its result type becomes + # value-dependent -- not representable statically (step-9 divergence) + args <- list(t3 = c(TRUE, FALSE, TRUE)) + for (m in names(grid_modes)) { + args[[paste0("y", m)]] <- grid_operand("a", "gen", set, m, "vec3") + args[[paste0("n", m)]] <- grid_operand("b", "gen", set, m, "vec3") + args[[paste0("p", m)]] <- grid_operand("a", "gen", set, m, "scl") + args[[paste0("q", m)]] <- grid_operand("b", "gen", set, m, "scl") + } + expect_grid_cells_match(qfn, fn, args, context = paste0("ifelse/", set)) + } +}) + +test_that("ifelse grid: symbolic branch lengths get a runtime guard", { + ids <- character() + stmts <- character() + for (my in names(grid_modes)) { + for (mn in names(grid_modes)) { + id <- paste0("r_", my, mn) + ids <- c(ids, id) + stmts <- c( + stmts, + paste0(" ", id, " <- ifelse(t1, y", my, ", n", mn, ")") + ) + } + } + src <- paste0( + "function(t1, yl, yi, yd, nl, ni, nd) {\n", + " declare(\n", + " type(t1 = logical(NA)),\n", + " type(yl = logical(NA)), type(yi = integer(NA)), type(yd = double(NA)),\n", + " type(nl = logical(NA)), type(ni = integer(NA)), type(nd = double(NA))\n", + " )\n", + paste(stmts, collapse = "\n"), + "\n list(\n ", + paste(paste0(ids, " = ", ids), collapse = ",\n "), + "\n )\n}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + + make_args <- function(len_n) { + args <- list(t1 = c(TRUE, FALSE, TRUE)) + for (m in names(grid_modes)) { + args[[paste0("y", m)]] <- grid_operand( + "a", + "gen", + "primary", + m, + "sym", + 3L + ) + args[[paste0("n", m)]] <- grid_operand( + "b", + "gen", + "primary", + m, + "sym", + len_n + ) + } + args + } + args_ok <- make_args(3L) + expect_grid_cells_match(qfn, fn, args_ok, context = "ifelse/sym") + # was: unguarded merge() reading past the shorter branch + expect_error( + do.call(qfn, make_args(4L)), + "must be scalars or match the shape", + fixed = TRUE + ) +}) + +test_that("ifelse grid: matrix test shapes the result", { + src <- paste0( + "function(tm, ym, nm, yi, pd) {\n", + " declare(\n", + " type(tm = logical(3, 2)),\n", + " type(ym = double(3, 2)), type(nm = double(3, 2)),\n", + " type(yi = integer(3, 2)), type(pd = double(1))\n", + " )\n", + " r_dd <- ifelse(tm, ym, nm)\n", + " r_id <- ifelse(tm, yi, nm)\n", + " r_sd <- ifelse(tm, pd, nm)\n", + " list(r_dd = r_dd, r_id = r_id, r_sd = r_sd)\n", + "}" + ) + fn <- eval(parse(text = src)[[1L]]) + dll_paths_before <- loaded_dll_paths() + on.exit(cleanup_new_quick_dlls(dll_paths_before), add = TRUE) + qfn <- quick(fn) + args <- list( + tm = matrix(c(TRUE, FALSE, TRUE, FALSE, TRUE, TRUE), 3L, 2L), + ym = grid_operand("a", "gen", "primary", "d", "mat32"), + nm = grid_operand("b", "gen", "primary", "d", "mat32"), + yi = grid_operand("a", "gen", "primary", "i", "mat32"), + pd = grid_operand("a", "gen", "primary", "d", "scl") + ) + expect_grid_cells_match(qfn, fn, args, context = "ifelse/mat") +}) + +test_that("ifelse contract violations are compile errors", { + scalar_test <- eval(parse( + text = paste0( + "function(t1, y, n) {\n", + " declare(type(t1 = logical(1)), type(y = double(3)), type(n = double(3)))\n", + " ifelse(t1, y, n)\n}" + ) + )[[1L]]) + expect_error(quick(scalar_test), "scalar test is not supported", fixed = TRUE) + + known_mismatch <- eval(parse( + text = paste0( + "function(t3, y, n) {\n", + " declare(type(t3 = logical(3)), type(y = double(4)), type(n = double(3)))\n", + " ifelse(t3, y, n)\n}" + ) + )[[1L]]) + expect_error( + quick(known_mismatch), + "must be scalars or match the shape", + fixed = TRUE + ) + + rank_mismatch <- eval(parse( + text = paste0( + "function(t3, y, n) {\n", + " declare(type(t3 = logical(3)), type(y = double(3, 2)), type(n = double(3)))\n", + " ifelse(t3, y, n)\n}" + ) + )[[1L]]) + expect_error( + quick(rank_mismatch), + "must be scalars or match the shape", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-declare-type.R b/tests/testthat/test-declare-type.R index 9c54eb4..10dc437 100644 --- a/tests/testthat/test-declare-type.R +++ b/tests/testthat/test-declare-type.R @@ -49,3 +49,14 @@ test_that("declare(type()) variants", { }) expect_quick_identical(quick_seq, list(1L, 5L)) }) + +test_that("character declarations are refused with a clean message", { + fn <- function(x) { + declare(type(x = character(1))) + x + } + expect_error( + quick(fn), + "character values are not supported by quickr" + ) +}) diff --git a/tests/testthat/test-errors.R b/tests/testthat/test-errors.R index eea0460..4917473 100644 --- a/tests/testthat/test-errors.R +++ b/tests/testthat/test-errors.R @@ -159,13 +159,36 @@ test_that("declare() type() calls validate syntax", { fixed = TRUE ) - bad_mode <- function(x) { + # A bare atomic mode symbol is a form error (dims are missing), not a + # mode error; the old check fired "only atomic modes are supported" + # precisely when the mode *was* atomic. + missing_dims <- function(x) { declare(type(x = double)) x } expect_error( - quick(bad_mode), - "only atomic modes are supported", + quick(missing_dims), + "the mode must be a call with dimensions, as in: type(x = double())", + fixed = TRUE + ) + + bad_mode_call <- function(x) { + declare(type(x = foo(1))) + x + } + expect_error( + quick(bad_mode_call), + "only atomic modes are supported, not: foo", + fixed = TRUE + ) + + bad_mode_symbol <- function(x) { + declare(type(x = foo)) + x + } + expect_error( + quick(bad_mode_symbol), + "only atomic modes are supported, not: foo", fixed = TRUE ) }) @@ -198,3 +221,73 @@ test_that("assigning an expression that produces no value errors cleanly", { fixed = TRUE ) }) + +test_that("unsupported complex operations are refused with R's messages", { + # Order comparisons on complex values: R errors, so must quickr -- with + # a clean message, not a raw gfortran failure. + complex_lt <- function(x, y) { + declare(type(x = complex(1)), type(y = complex(1))) + x < y + } + expect_error(quick(complex_lt), "invalid comparison with complex values") + + # Equality is supported, as in R. + complex_eq <- function(x, y) { + declare(type(x = complex(1)), type(y = complex(1))) + x == y + } + expect_quick_identical(complex_eq, list(1i, 1i)) + expect_quick_identical(complex_eq, list(1i, 2i)) + + # modulo() has no complex form in Fortran; R refuses too. + complex_mod <- function(x, y) { + declare(type(x = complex(1)), type(y = complex(1))) + x %% y + } + expect_error(quick(complex_mod), "unimplemented complex operation") +}) + +test_that("complex operands are refused in linear algebra", { + # The real BLAS/LAPACK lowerings (dgemm, dgesv, ...) would read complex + # storage as reals and return a plausible wrong answer where R returns a + # complex result: complex(2) %*% complex(2) returned a real dot product + # of the real parts. Refuse at compile time instead. + complex_matmul <- function(x, y) { + declare(type(x = complex(2)), type(y = complex(2))) + x %*% y + } + expect_error( + quick(complex_matmul), + "%*% does not support complex operands", + fixed = TRUE + ) + + # One complex operand is enough to poison the d* routine. + complex_mixed <- function(x, y) { + declare(type(x = complex(2, 2)), type(y = double(2, 2))) + x %*% y + } + expect_error(quick(complex_mixed), "does not support complex operands") + + complex_solve <- function(x) { + declare(type(x = complex(2, 2))) + solve(x) + } + expect_error(quick(complex_solve), "does not support complex operands") + + complex_crossprod <- function(x) { + declare(type(x = complex(2, 2))) + crossprod(x) + } + expect_error(quick(complex_crossprod), "does not support complex operands") + + # t() alone is mode-preserving and keeps working on complex values. + complex_t <- function(x) { + declare(type(x = complex(2, 2))) + t(x) + } + expect_quick_identical( + complex_t, + list(matrix(c(1 + 1i, 2 + 0i, 3 - 1i, 4 + 2i), 2, 2)) + ) +}) diff --git a/tests/testthat/test-flang-preference.R b/tests/testthat/test-flang-preference.R index 4fc4982..6b85d7d 100644 --- a/tests/testthat/test-flang-preference.R +++ b/tests/testthat/test-flang-preference.R @@ -87,6 +87,11 @@ test_that("quickr_fcompiler_env returns empty when disabled or unavailable", { }) test_that("quickr_fcompiler_env uses caller-provided sysname for flang auto preference", { + state <- quickr:::quickr_flang_state + old_auto_disabled <- state$auto_disabled + state$auto_disabled <- FALSE + on.exit(state$auto_disabled <- old_auto_disabled, add = TRUE) + temp <- withr::local_tempdir() prefix <- file.path(temp, "flang") dir.create(file.path(prefix, "bin"), recursive = TRUE) diff --git a/tests/testthat/test-flatten-vector.R b/tests/testthat/test-flatten-vector.R new file mode 100644 index 0000000..7fe2e30 --- /dev/null +++ b/tests/testthat/test-flatten-vector.R @@ -0,0 +1,86 @@ +# c(matrix), as.vector(), and as.integer()/as.double() of arrays all +# flatten column-major, matching R's drop-dims semantics. + +skip_on_cran() + +test_that("c() flattens matrix and array arguments column-major", { + c_mat <- function(m) { + declare(type(m = double(2, 3))) + c(m) + } + expect_quick_identical(c_mat, list(matrix(as.double(1:6), 2, 3))) + + c_mixed <- function(m, v) { + declare(type(m = double(2, 2)), type(v = double(3))) + c(m, v) + } + expect_quick_identical( + c_mixed, + list(matrix(as.double(1:4), 2, 2), c(7, 8, 9)) + ) + + c_two_mats <- function(a, b) { + declare(type(a = integer(2, 2)), type(b = integer(1, 3))) + c(a, b) + } + expect_quick_identical( + c_two_mats, + list(matrix(1:4, 2, 2), matrix(7:9, 1, 3)) + ) +}) + +test_that("as.vector() drops dimensions, preserving or coercing the mode", { + # default mode preserves type + av_dbl <- function(m) { + declare(type(m = double(2, 3))) + as.vector(m) + } + expect_quick_identical(av_dbl, list(matrix(as.double(1:6), 2, 3))) + + av_lgl <- function(m) { + declare(type(m = logical(2, 2))) + as.vector(m) + } + expect_quick_identical(av_lgl, list(matrix(c(TRUE, FALSE, TRUE, TRUE), 2, 2))) + + # mode = "double" delegates to as.double() + av_coerce <- function(m) { + declare(type(m = integer(2, 2))) + as.vector(m, mode = "double") + } + expect_quick_identical(av_coerce, list(matrix(1:4, 2, 2))) +}) + +test_that("as.vector() refuses unsupported modes and non-constant modes", { + missing_x <- function() { + as.vector() + } + expect_error( + quick(missing_x), + "as.vector() expects `x`", + fixed = TRUE + ) + + bad_mode <- function(m) { + declare(type(m = double(2))) + as.vector(m, mode = "list") + } + expect_error( + quick(bad_mode), + "as.vector() does not support mode", + fixed = TRUE + ) +}) + +test_that("as.integer() drops dimensions for an int-backed logical matrix", { + # External logical args are integer-backed; the flatten must still apply + # (it used to be skipped by an early return, keeping the matrix dims). + fn <- function(m) { + declare(type(m = logical(2, 3))) + as.integer(m) + } + expect_quick_identical( + fn, + list(matrix(c(TRUE, FALSE, TRUE, FALSE, TRUE, TRUE), 2, 3)) + ) +}) diff --git a/tests/testthat/test-hoist-mask.R b/tests/testthat/test-hoist-mask.R index b4c7ed7..fe4b103 100644 --- a/tests/testthat/test-hoist-mask.R +++ b/tests/testthat/test-hoist-mask.R @@ -50,3 +50,26 @@ test_that("hoist mask", { expect_equal(qfn(x), fn(x)) # bench::mark(qfn(x), fn(x), relative = T) }) + +test_that("any()/all() drop an inherited hoist_mask from an enclosing reduction", { + # An enclosing numeric reduction threads its own hoist_mask through + # `...`; any()/all() must install a fresh mask hoister for their own + # argument instead of forwarding both (which handed the `[` handler + # two hoist_mask arguments). + fn <- function(x, m) { + declare(type(x = double(n)), type(m = logical(n))) + out <- sum(x * as.double(any(x[m] > 1))) + out + } + x <- c(0.5, 2, 3) + expect_quick_identical(fn, list(x, c(TRUE, FALSE, TRUE))) + expect_quick_identical(fn, list(x, c(TRUE, FALSE, FALSE))) + + fn_all <- function(x, m) { + declare(type(x = double(n)), type(m = logical(n))) + out <- sum(x * as.double(all(x[m] > 1))) + out + } + expect_quick_identical(fn_all, list(x, c(FALSE, TRUE, TRUE))) + expect_quick_identical(fn_all, list(x, c(TRUE, FALSE, TRUE))) +}) diff --git a/tests/testthat/test-internal-utils.R b/tests/testthat/test-internal-utils.R index 977a805..30a0169 100644 --- a/tests/testthat/test-internal-utils.R +++ b/tests/testthat/test-internal-utils.R @@ -66,17 +66,9 @@ test_that("discard and drop_nulls behave as expected", { expect_identical(names(quickr:::drop_nulls(x, c("a", "c"))), c("b", "c")) }) -test_that("new_function and str_flatten_args are usable", { +test_that("new_function is usable", { f <- quickr:::new_function(args = alist(x = ), body = quote(x + 1L)) expect_identical(f(1L), 2L) - - expect_identical( - quickr:::str_flatten_args("a", "b", multiline = FALSE), - "a,b" - ) - expect_true( - grepl("\n", quickr:::str_flatten_args("a", "b", "c", multiline = TRUE)) - ) }) test_that("parent.pkg detects namespaces and set_names mutates names", { @@ -243,7 +235,7 @@ test_that("print.quickr_ordered_env outputs bindings", { test_that("check_assignment_compatible handles NULL value", { target <- quickr:::Variable("double", list(1L)) - expect_silent(quickr:::check_assignment_compatible(target, NULL)) + expect_silent(quickr:::check_assignment_compatible("x", target, NULL)) }) test_that("r2size() warns, not crashes, on a deferred-mode Variable", { diff --git a/tests/testthat/test-logical.R b/tests/testthat/test-logical.R index bd157f5..a8a839a 100644 --- a/tests/testthat/test-logical.R +++ b/tests/testthat/test-logical.R @@ -110,3 +110,106 @@ test_that("parentheses preserve logical precedence", { expect_quick_identical(fn_a, !!!cases) expect_quick_identical(fn_b, !!!cases) }) + +test_that("&& and || require length-1 operands, like R", { + vec_and <- function(x, y) { + declare(type(x = logical(3)), type(y = logical(3))) + x && y + } + expect_error(quick(vec_and), "length-1 operands") + + vec_or <- function(x, y) { + declare(type(x = logical(n)), type(y = logical(n))) + x || y + } + expect_error(quick(vec_or), "length-1 operands") + + numeric_and <- function(a, b) { + declare(type(a = double(1)), type(b = double(1))) + a && b + } + expect_error(quick(numeric_and), "logical operands") + + vector_matrix_and <- function(x, y) { + declare(type(x = logical(2)), type(y = logical(2, 2))) + x && y + } + expect_error(quick(vector_matrix_and), "requires length-1 operands") + + matrix_vector_or <- function(x, y) { + declare(type(x = logical(2, 2)), type(y = logical(2))) + x || y + } + expect_error(quick(matrix_vector_or), "requires length-1 operands") +}) + +test_that("&& and || accept one-element matrices", { + matrix_and <- function(x, y) { + declare(type(x = logical(1, 1)), type(y = logical(1, 1))) + x && y + } + matrix_or <- function(x, y) { + declare(type(x = logical(1, 1)), type(y = logical(1, 1))) + x || y + } + + true <- matrix(TRUE, 1, 1) + false <- matrix(FALSE, 1, 1) + expect_quick_identical(matrix_and, list(true, true), list(true, false)) + expect_quick_identical(matrix_or, list(false, false), list(false, true)) +}) + +test_that("&& and || short-circuit like R's scalar operators", { + # The right operand indexes past the end of x whenever the left side + # already decides; R never evaluates it. + guarded_index <- function(i, x) { + declare(type(i = integer(1)), type(x = double(3))) + out <- 0 + if (i <= 3L && x[i] > 0) { + out <- 1 + } + out + } + expect_translation_snapshots(guarded_index) + expect_quick_identical(guarded_index, list(5L, c(1, 2, 3))) + expect_quick_identical(guarded_index, list(2L, c(1, 2, 3))) + expect_quick_identical(guarded_index, list(2L, c(1, -2, 3))) + + or_guarded <- function(a, x, i) { + declare(type(a = logical(1)), type(x = double(2)), type(i = integer(1))) + out <- a || x[i] > 0 + out + } + expect_quick_identical(or_guarded, list(TRUE, c(1, 2), 9L)) + expect_quick_identical(or_guarded, list(FALSE, c(-1, 2), 2L)) + + shadowed_abs <- function() { + calls <- 0L + abs <- function() { + calls <<- calls + 1L + TRUE + } + and_result <- FALSE && abs() + or_result <- TRUE || abs() + list(and_result = and_result, or_result = or_result, calls = calls) + } + expect_quick_identical(shadowed_abs, list()) +}) + +test_that("while re-evaluates hoisted condition code every iteration", { + # The canonical scan idiom: the && lowering hoists statements, which + # must re-run per iteration, not once before the loop. + scan_positive <- function(x) { + declare(type(x = double(n))) + i <- 1L + n <- length(x) + while (i <= n && x[i] > 0) { + i <- i + 1L + } + i + } + expect_translation_snapshots(scan_positive) + expect_quick_identical(scan_positive, list(c(1, 2, -1, 5))) + expect_quick_identical(scan_positive, list(c(1, 2, 3))) + expect_quick_identical(scan_positive, list(c(-1, 2))) +}) diff --git a/tests/testthat/test-loops.R b/tests/testthat/test-loops.R index 7896836..90abbf0 100644 --- a/tests/testthat/test-loops.R +++ b/tests/testthat/test-loops.R @@ -103,3 +103,41 @@ test_that("expr return value", { expect_translation_snapshots(fn) expect_quick_identical(fn, 1:10) }) + +test_that("single-statement while/repeat bodies re-run their hoisted statements", { + # A non-`{` loop body whose lone statement hoists code (here a BLAS + # call) must emit that code inside the loop; hoisting it out of the + # loop would freeze the body's work at its first evaluation. `for` is + # covered in test-for-iterables.R. + # + # Keep the direct repeat assignment as a translation regression because + # it cannot terminate. Exercise the generated repeat path separately with + # a bounded one-statement body whose else branch repeats the same BLAS work. + # fmt: skip + squarings_while <- function(m) { + declare(type(m = double(2, 2))) + while (m[1, 1] < 100) m <- m %*% m + m + } + + expect_translation_snapshots(squarings_while) + expect_quick_identical(squarings_while, list(diag(2) * 2)) + + # fmt: skip + squarings_repeat <- function(m) { + declare(type(m = double(2, 2))) + repeat m <- m %*% m + m + } + + expect_translation_snapshots(squarings_repeat) + + # fmt: skip + bounded_squarings_repeat <- function(m) { + declare(type(m = double(2, 2))) + repeat if (m[1, 1] >= 100) break else m <- m %*% m + m + } + + expect_quick_identical(bounded_squarings_repeat, list(diag(2) * 2)) +}) diff --git a/tests/testthat/test-matrix-inference.R b/tests/testthat/test-matrix-inference.R index 7fd3923..0af431e 100644 --- a/tests/testthat/test-matrix-inference.R +++ b/tests/testthat/test-matrix-inference.R @@ -137,7 +137,7 @@ test_that("matrix ops infer destination sizes for assignments", { expect_quick_equal(chol2inv_infer, list(A = A_pd)) }) -test_that("crossprod requires conformability at compile time", { +test_that("crossprod with unverifiable dims compiles and guards at runtime", { fn <- function(x, y, n, p, m, k) { declare( type(n = integer(1)), @@ -150,10 +150,17 @@ test_that("crossprod requires conformability at compile time", { crossprod(x, y) } + # was: hard compile error "cannot verify conformability in crossprod" + qfn <- quick(fn) + x <- matrix(as.double(1:6), 2, 3) + y <- matrix(as.double(6:1), 2, 3) + expect_equal( + qfn(x, y, 2L, 2L, 3L, 3L), + crossprod(x, y) + ) expect_error( - quick(fn), - "cannot verify conformability in crossprod", - fixed = TRUE + qfn(x, matrix(as.double(1:6), 3, 2), 2L, 3L, 3L, 2L), + "non-conformable arguments in crossprod" ) }) @@ -212,8 +219,8 @@ test_that("matrix helpers report unsupported inputs", { expect_error(quick(back_bad_B), "triangular solve only supports vector") }) -test_that("matrix conformability warnings are surfaced", { - matmul_warn <- function(A, B, n, m, k) { +test_that("unverifiable %*% dims compile without warning and guard at runtime", { + matmul_unknown <- function(A, B, n, m, k) { declare( type(n = integer(1)), type(m = integer(1)), @@ -224,8 +231,14 @@ test_that("matrix conformability warnings are surfaced", { A %*% B } - expect_warning( - quick(matmul_warn), - "cannot verify conformability in %\\*%" + # was: compile-time R warning, then an unchecked BLAS call + qfn <- expect_no_warning(quick(matmul_unknown)) + A <- matrix(as.double(1:6), 2, 3) + B <- matrix(as.double(6:1), 3, 2) + expect_equal(qfn(A, B, 2L, 3L, 3L), A %*% B) + expect_error( + qfn(A, matrix(as.double(1:4), 2, 2), 2L, 3L, 2L), + "non-conformable arguments in %*%", + fixed = TRUE ) }) diff --git a/tests/testthat/test-matrix-lapack.R b/tests/testthat/test-matrix-lapack.R index a8b76d6..0036b97 100644 --- a/tests/testthat/test-matrix-lapack.R +++ b/tests/testthat/test-matrix-lapack.R @@ -62,8 +62,9 @@ test_that("solve handles column RHS matrices and 1x1 systems", { expect_quick_equal(solve_scalar, list(A = matrix(2.5, 1L, 1L), b = 1.25)) }) -test_that("solve supports least-squares for rectangular systems", { - solve_ls_vec <- function(X, y) { +test_that("solve requires a square coefficient matrix, like R", { + # Squareness unknown at compile time: a runtime guard runs before dgesv. + solve_sym <- function(X, y) { declare( type(X = double(n, k)), type(y = double(n)) @@ -71,48 +72,37 @@ test_that("solve supports least-squares for rectangular systems", { solve(X, y) } - solve_ls_mat <- function(X, Y) { - declare( - type(X = double(n, k)), - type(Y = double(n, p)) - ) - solve(X, Y) - } - set.seed(123) n <- 20 k <- 5 - p <- 3 X <- matrix(rnorm(n * k), n, k) y <- rnorm(n) - Y <- matrix(rnorm(n * p), n, p) - q_solve_ls_vec <- expect_warning(quick(solve_ls_vec), NA) - q_solve_ls_mat <- expect_warning(quick(solve_ls_mat), NA) + q_solve_sym <- quick(solve_sym) + expect_error(q_solve_sym(X, y), "solve requires a square matrix") + expect_error(solve(X, y), "must be square") # the R oracle errors too - expect_equal(q_solve_ls_vec(X, y), qr.solve(X, y)) - expect_equal(q_solve_ls_mat(X, Y), qr.solve(X, Y)) -}) + base <- matrix(rnorm(n * n), n, n) + A <- crossprod(base) + diag(n) + expect_equal(q_solve_sym(A, y), solve(A, y)) -test_that("solve supports least-squares for single-column systems", { - solve_ls_col <- function(X, y) { + # One statically known axis still guards the symbolic one. + solve_col <- function(X, y) { declare( type(X = double(n, 1L)), type(y = double(n)) ) solve(X, y) } - - set.seed(125) - n <- 20 - X <- matrix(rnorm(n), n, 1L) - y <- rnorm(n) - - q_solve_ls_col <- expect_warning(quick(solve_ls_col), NA) - expect_equal(q_solve_ls_col(X, y), qr.solve(X, y)) + q_solve_col <- quick(solve_col) + expect_error(q_solve_col(X[, 1L, drop = FALSE], y), "square matrix") + expect_equal( + q_solve_col(matrix(2.5, 1L, 1L), 1.25), + solve(matrix(2.5, 1L, 1L), 1.25) + ) }) -test_that("solve compiles 1-row least-squares systems", { +test_that("solve rejects statically rectangular systems at compile time", { solve_one_row <- function(X, y) { declare( type(X = double(1L, 2L)), @@ -120,8 +110,7 @@ test_that("solve compiles 1-row least-squares systems", { ) solve(X, y) } - - expect_no_error(r2f(solve_one_row)) + expect_error(r2f(solve_one_row), "solve requires a square matrix") }) 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", { expect_false(has_call(square_fortran, "dgesv")) }) -test_that("solve uses dgesv for known square and dgels for rectangular systems", { +test_that("solve always uses dgesv, guarding squareness when symbolic", { solve_square <- function(A, b) { declare( type(A = double(n, n)), @@ -212,12 +201,7 @@ test_that("solve uses dgesv for known square and dgels for rectangular systems", solve(A, b) } - solve_rect <- function(A, b) { - declare(type(A = double(3, 2)), type(b = double(3))) - solve(A, b) - } - - solve_rect_named <- function(A, b) { + solve_sym <- function(A, b) { declare( type(A = double(n, k)), type(b = double(n)) @@ -233,12 +217,8 @@ test_that("solve uses dgesv for known square and dgels for rectangular systems", capture.output(cat(r2f(solve_square_fixed))), collapse = "\n" ) - rect_fixed_fortran <- paste( - capture.output(cat(r2f(solve_rect))), - collapse = "\n" - ) - rect_named_fortran <- paste( - capture.output(cat(r2f(solve_rect_named))), + sym_fortran <- paste( + capture.output(cat(r2f(solve_sym))), collapse = "\n" ) @@ -246,19 +226,18 @@ test_that("solve uses dgesv for known square and dgels for rectangular systems", any(grepl(paste0("call ", routine, "("), tolower(code), fixed = TRUE)) } - # Search emitted Fortran to ensure solve() chooses LU (dgesv) for proven-square - # systems and least-squares (dgels) for rectangular systems. + # solve() is LU (dgesv) only; least squares is qr.solve()'s job. Provably + # square systems get no guard, symbolic squareness is checked at run time. expect_true(has_call(square_named_fortran, "dgesv")) expect_false(has_call(square_named_fortran, "dgels")) + expect_false(grepl("square matrix", square_named_fortran, fixed = TRUE)) expect_true(has_call(square_fixed_fortran, "dgesv")) expect_false(has_call(square_fixed_fortran, "dgels")) - expect_true(has_call(rect_fixed_fortran, "dgels")) - expect_false(has_call(rect_fixed_fortran, "dgesv")) - - expect_true(has_call(rect_named_fortran, "dgels")) - expect_false(has_call(rect_named_fortran, "dgesv")) + expect_true(has_call(sym_fortran, "dgesv")) + expect_false(has_call(sym_fortran, "dgels")) + expect_match(sym_fortran, "solve requires a square matrix", fixed = TRUE) }) test_that("chol and chol2inv match R", { @@ -334,6 +313,54 @@ test_that("diag matches R for vectors, matrices, and sizes", { expect_quick_equal(diag_named_order, list(x = c(1, 2))) }) +test_that("diag sizes identities from length-one expressions", { + diag_c <- function() { + out <- diag(c(3L)) + out + } + diag_parens <- function() { + out <- diag((3L)) + out + } + diag_logical <- function() { + out <- diag(c(TRUE)) + out + } + diag_computed <- function() { + out <- diag(abs(-3L)) + out + } + + expect_quick_equal(diag_c, list()) + expect_quick_equal(diag_parens, list()) + expect_quick_equal(diag_logical, list()) + expect_quick_equal(diag_computed, list()) +}) + +test_that("diag size folding rejects shadowed local closures", { + shadow_c <- function() { + c <- function(x) 2L + out <- diag(c(3L)) + out + } + expect_error( + quick(shadow_c), + "local closure `c()` cannot determine a result size", + fixed = TRUE + ) + + shadow_abs <- function() { + abs <- function(x) 2L + out <- diag(abs(-3L)) + out + } + expect_error( + quick(shadow_abs), + "local closure `abs()` cannot determine a result size", + fixed = TRUE + ) +}) + test_that("diag handles missing x with nrow/ncol and 1x1 matrices", { diag_nrow <- function(n) { declare(type(n = integer(1))) @@ -415,7 +442,7 @@ test_that("Test bad path in lapack functions", { } expect_error(quick(solve_bad_rank), "expects a matrix") - expect_warning(quick(solve_non_square), NA) + expect_error(quick(solve_non_square), "requires a square matrix") expect_error(quick(solve_bad_rhs), "only supports vector or matrix") expect_error(quick(chol_bad_rank), "expects a matrix") expect_error(quick(chol_pivot), "pivot = TRUE") @@ -446,7 +473,7 @@ test_that("lapack functions test non-square matrix errors", { chol2inv(R) } - expect_warning(quick(solve_rect), NA) + expect_error(quick(solve_rect), "requires a square matrix") expect_error(quick(chol_rect), "requires a square matrix") expect_error(quick(chol2inv_rect), "requires a square matrix") }) diff --git a/tests/testthat/test-matrix-mul.R b/tests/testthat/test-matrix-mul.R index 7bf6b69..a9b2813 100644 --- a/tests/testthat/test-matrix-mul.R +++ b/tests/testthat/test-matrix-mul.R @@ -227,7 +227,13 @@ test_that("matrix multiplication rejects incompatible destinations", { x } - expect_error(quick(dest_mismatch), "incompatible rank for %\\*%") + # The destination's declared rank no longer matches the result; the + # reassignment shape check rejects it (routing through a temp first). + expect_error( + quick(dest_mismatch), + "replacement rank (2) differs", + fixed = TRUE + ) }) test_that("BLAS matrix ops coerce integer and logical inputs to double", { @@ -460,7 +466,29 @@ test_that("crossprod rejects incompatible destination dimensions", { expect_error( quick(crossprod_bad_dest), - "assignment target has incompatible dimensions" + "dimension 1 would change from 2 to 3", + fixed = TRUE + ) +}) + +test_that("BLAS output routes through a temp when the dest shape is unproven", { + # A pre-declared destination whose symbolic dims are not *proven* equal + # to the BLAS result shape must not be written directly (that passed the + # wrong leading dimensions to dsyrk and could corrupt or overflow the + # output). crossprod(x) here is 3x3; the (m, m) dest gets a runtime + # shape guard instead of a silent direct write. + fn <- function(x, m) { + declare(type(x = double(n, 3)), type(m = integer(1))) + out <- matrix(0, m, m) + out <- crossprod(x) + out + } + qfn := quick(fn) + x <- matrix(as.double(1:6), nrow = 2L) + expect_equal(qfn(x, 3L), crossprod(x)) + expect_error( + qfn(x, 4L), + "reassignment must preserve the shape of `out`" ) }) @@ -665,6 +693,18 @@ test_that("forwardsolve and backsolve match R", { backsolve(L, b, upper.tri = FALSE) } + back_alias_coefficient <- function(A) { + declare(type(A = double(4, 4))) + A <- backsolve(A, A, transpose = TRUE) + A + } + + forward_alias_coefficient <- function(A) { + declare(type(A = double(4, 4))) + A <- forwardsolve(A, A, transpose = TRUE) + A + } + set.seed(11) base <- matrix(rnorm(16), nrow = 4) L <- base @@ -684,4 +724,6 @@ test_that("forwardsolve and backsolve match R", { expect_quick_equal(forward_upper, list(U = U, b = b_vec)) expect_quick_equal(forward_transpose, list(L = L, b = b_vec)) expect_quick_equal(back_lower, list(L = L, b = b_vec)) + expect_quick_equal(back_alias_coefficient, list(A = U)) + expect_quick_equal(forward_alias_coefficient, list(A = L)) }) diff --git a/tests/testthat/test-matrix.R b/tests/testthat/test-matrix.R index 9505797..aacdd7b 100644 --- a/tests/testthat/test-matrix.R +++ b/tests/testthat/test-matrix.R @@ -212,7 +212,7 @@ test_that("elementwise vector operations require matching lengths", { expect_error( quick(fn), - "elementwise vector operations require lengths that recycle cleanly unless one operand is scalar", + "elementwise vector operations require equal lengths or a scalar operand; R-style recycling is not supported", fixed = TRUE ) }) @@ -348,6 +348,88 @@ test_that("t() and diag() preserve integer mode", { expect_quick_equal(dident, list()) }) +test_that("diag() takes R's identity form for any length-1 x", { + # R's rule is length(x) == 1 with no nrow/ncol, not rank 0: a declared + # integer(1) argument is the n x n identity, not a 1x1 matrix holding n + dsym <- function(n) { + declare(type(n = integer(1))) + diag(n) + } + expect_quick_equal(dsym, list(3L), list(1L)) + + # same through the inferred-destination path + ddest <- function(n) { + declare(type(n = integer(1))) + out <- diag(n) + out + } + expect_quick_equal(ddest, list(3L)) + + # a size expression works too + dexpr <- function(n) { + declare(type(n = integer(1))) + diag(n + 1L) + } + expect_quick_equal(dexpr, list(2L)) + + # a length-1 vector is still length 1 + dvec1 <- function(v) { + declare(type(v = integer(1))) + diag(v) + } + expect_quick_equal(dvec1, list(3L)) + + # nrow/ncol switch off the identity form, as R's nargs() rule does + d1x1 <- function(v) { + declare(type(v = double(1))) + diag(v, 1L) + } + expect_quick_equal(d1x1, list(3)) + + # longer vectors keep building a diagonal matrix + dlong <- function(v) { + declare(type(v = double(3))) + diag(v) + } + expect_quick_equal(dlong, list(c(1, 2, 3))) + + # R sizes the identity with as.integer(x), so a double or logical x works + # and truncates toward zero + ddbl <- function(x) { + declare(type(x = double(1))) + diag(x) + } + expect_quick_equal(ddbl, list(3), list(3.7), list(1)) + + ddbl_dest <- function(x) { + declare(type(x = double(1))) + out <- diag(x) + out + } + expect_quick_equal(ddbl_dest, list(3)) + + dlgl <- function(b) { + declare(type(b = logical(1))) + diag(b) + } + expect_quick_equal(dlgl, list(TRUE)) + + # a non-whole literal truncates too, rather than tripping the + # "size must be an integer" check + dfrac <- function() { + out <- diag(3.7) + out + } + expect_quick_equal(dfrac, list()) + + # an explicit coercion at the call site is the same program + dcoerce <- function(x) { + declare(type(x = double(1))) + diag(as.integer(x)) + } + expect_quick_equal(dcoerce, list(3)) +}) + test_that("t() and diag() preserve logical mode", { m <- matrix(c(TRUE, FALSE, TRUE, TRUE), 2, 2) @@ -391,3 +473,26 @@ test_that("diag() initializes integer-backed logical outputs as integers", { expect_translation_snapshots(fn) expect_quick_identical(fn, list(c(TRUE, FALSE))) }) + +test_that("matrix() refuses byrow=TRUE and dimnames cleanly", { + fn_byrow <- function() { + m <- matrix(1, nrow = 2, ncol = 2, byrow = TRUE) + m + } + expect_error( + quick(fn_byrow), + "matrix(byrow=TRUE) is not supported", + fixed = TRUE + ) + + # dimnames used to be silently dropped (R keeps them); refuse like array() + fn_dimnames <- function() { + m <- matrix(1, nrow = 2, ncol = 2, dimnames = list(c("a", "b"), NULL)) + m + } + expect_error( + quick(fn_dimnames), + "matrix(dimnames=) not supported", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-qr-solve.R b/tests/testthat/test-qr-solve.R index 101d1d2..ace49af 100644 --- a/tests/testthat/test-qr-solve.R +++ b/tests/testthat/test-qr-solve.R @@ -56,3 +56,71 @@ test_that("qr.solve quick matches base R", { b_wide <- rnorm(5) expect_quick_equal(qr_solve_vec, list(a_wide, b_wide)) }) + +test_that("qr.solve supports a known zero-width matrix right-hand side", { + fn <- function(a, b) { + declare(type(a = double(3, 2)), type(b = double(3, 0))) + qr.solve(a, b) + } + a <- rbind(c(1, 0), c(0, 1), c(1, 1)) + b <- matrix(double(), 3, 0) + + expect_false(grepl("call dqrcf", as.character(r2f(fn)), fixed = TRUE)) + expect_quick_equal(fn, list(a, b)) +}) + +test_that("qr.solve supports a dynamic zero-width matrix right-hand side", { + fn <- function(a, b) { + declare(type(a = double(3, 2)), type(b = double(3, NA))) + qr.solve(a, b) + } + a <- rbind(c(1, 0), c(0, 1), c(1, 1)) + b <- matrix(double(), 3, 0) + + expect_quick_equal(fn, list(a, b)) +}) + +test_that("qr.solve still rejects rank deficiency with a zero-width RHS", { + fn <- function(a, b) { + declare(type(a = double(3, 2)), type(b = double(3, 0))) + qr.solve(a, b) + } + a <- cbind(as.double(1:3), as.double(1:3)) + b <- matrix(double(), 3, 0) + qfn <- expect_no_warning(quick(fn)) + + expect_error(qfn(a, b), "rank deficient matrix in qr.solve", fixed = TRUE) +}) + +test_that("qr.solve rejects coefficient matrices with zero extents", { + message <- "qr.solve coefficient matrices with zero extents are not supported" + known_zero_rows <- function(a, b) { + declare(type(a = double(0, 2)), type(b = double(0, 0))) + qr.solve(a, b) + } + known_zero_cols <- function(a, b) { + declare(type(a = double(2, 0)), type(b = double(2, 0))) + qr.solve(a, b) + } + dynamic <- function(a, b) { + declare(type(a = double(NA, NA)), type(b = double(NA, NA))) + qr.solve(a, b) + } + + expect_error(quick(known_zero_rows), message, fixed = TRUE) + expect_error(quick(known_zero_cols), message, fixed = TRUE) + + code <- r2f(dynamic) + expect_match(as.character(code), message, fixed = TRUE) + qfn <- expect_no_warning(quick(dynamic)) + expect_error( + qfn(matrix(double(), 0, 2), matrix(double(), 0, 0)), + message, + fixed = TRUE + ) + expect_error( + qfn(matrix(double(), 2, 0), matrix(double(), 2, 0)), + message, + fixed = TRUE + ) +}) diff --git a/tests/testthat/test-r2f-registry.R b/tests/testthat/test-r2f-registry.R index 137d3ea..c2a89ee 100644 --- a/tests/testthat/test-r2f-registry.R +++ b/tests/testthat/test-r2f-registry.R @@ -62,6 +62,75 @@ test_that("register_r2f_handler does not set match.fun when TRUE", { expect_null(result@match_fun) }) +test_that("register_r2f_handler records the name of a namespace-level handler", { + withr::defer( + rm(list = "test_handler_named", envir = quickr:::r2f_handlers), + envir = environment() + ) + # Passed as a bare symbol, the way the package's own top-level registrations + # do it -- `quickr:::last` would be a call, with no name to record. + result <- quickr:::register_r2f_handler("test_handler_named", last) + expect_identical(result@fun_name, "last") +}) + +test_that("register_r2f_handler leaves anonymous and local handlers unnamed", { + local_handler <- function(e, scope, ...) NULL + withr::defer( + rm( + list = c("test_handler_unnamed_anon", "test_handler_unnamed_local"), + envir = quickr:::r2f_handlers + ), + envir = environment() + ) + anon <- quickr:::register_r2f_handler( + "test_handler_unnamed_anon", + function(e, scope, ...) NULL + ) + # A symbol, but bound in a call frame rather than a namespace, so the name + # would mean something else the next time the frame is entered. + local <- quickr:::register_r2f_handler( + "test_handler_unnamed_local", + local_handler + ) + expect_null(anon@fun_name) + expect_null(local@fun_name) +}) + +test_that("dispatch re-resolves a named handler's namespace binding", { + # covr rebinds its instrumented copies into the namespace after the package + # has loaded -- that is, after registration captured the function object. + # Mocking the binding reproduces that sequence exactly. + original <- last + withr::defer( + rm(list = "test_handler_rebound", envir = quickr:::r2f_handlers), + envir = environment() + ) + registered <- quickr:::register_r2f_handler("test_handler_rebound", last) + expect_identical(registered@fun_name, "last") + + local_mocked_bindings(last = function(x) "rebound") + resolved <- quickr:::get_r2f_handler(quote(test_handler_rebound)) + expect_identical(resolved("ignored"), "rebound") + + # Resolving hands back a copy; the registry still holds what was registered. + expect_identical( + S7::S7_data(quickr:::r2f_handlers[["test_handler_rebound"]]), + original + ) +}) + +test_that("dispatch leaves unnamed handlers alone", { + handler <- function(e, scope, ...) "anonymous" + withr::defer( + rm(list = "test_handler_untouched", envir = quickr:::r2f_handlers), + envir = environment() + ) + quickr:::register_r2f_handler("test_handler_untouched", handler) + resolved <- quickr:::get_r2f_handler(quote(test_handler_untouched)) + expect_null(resolved@fun_name) + expect_identical(S7::S7_data(resolved), handler) +}) + test_that("register_r2f_handler registers multiple names", { handler <- function(e, scope, ...) NULL withr::defer( diff --git a/tests/testthat/test-recycling.R b/tests/testthat/test-recycling.R new file mode 100644 index 0000000..e547dbe --- /dev/null +++ b/tests/testthat/test-recycling.R @@ -0,0 +1,448 @@ +# Elementwise conformability: R-style recycling is rejected (compile error +# for known mismatches, runtime guard for unknown), fill constructors spread +# inside c(), and matrix(scalar, m, n) materializes a real array. + +skip_on_cran() + +test_that("known unequal vector lengths are a compile error", { + # divisible lengths were previously blessed and silently mis-lowered + divisible <- function(a, b) { + declare(type(a = double(2)), type(b = double(4))) + a + b + } + expect_error(quick(divisible), "equal lengths") + + ragged <- function(a, b) { + declare(type(a = double(2)), type(b = double(3))) + a * b + } + expect_error(quick(ragged), "equal lengths") + + zero_len <- function(a, b) { + declare(type(a = double(0)), type(b = double(4))) + a + b + } + expect_error(quick(zero_len), "equal lengths") +}) + +test_that("a known length-0 operand is rejected against an unknown length", { + # R answers numeric(0) here; quickr has no length-0 result to return, so + # the zero is rejected even when the other operand's length is not a + # number the compiler can compare it to. + fill_left <- function(x) { + declare(type(x = double(n))) + numeric(0) + x + } + expect_error(quick(fill_left), "equal lengths") + + fill_right <- function(x) { + declare(type(x = double(n))) + x > numeric(0) + } + expect_error(quick(fill_right), "equal lengths") + + declared <- function(a, b) { + declare(type(a = double(0)), type(b = double(n))) + a * b + } + expect_error(quick(declared), "equal lengths") + + # An NA dim is unknown, not "matches anything" + unspecified <- function(a, b) { + declare(type(a = double(NA)), type(b = double(0))) + a - b + } + expect_error(quick(unspecified), "equal lengths") +}) + +test_that("length checks cover comparisons, logical ops, and modulo", { + comparison <- function(a, b) { + declare(type(a = double(2)), type(b = double(4))) + a < b + } + expect_error(quick(comparison), "equal lengths") + + logical_op <- function(a, b) { + declare(type(a = logical(2)), type(b = logical(4))) + a & b + } + expect_error(quick(logical_op), "equal lengths") + + modulo <- function(a, b) { + declare(type(a = integer(2)), type(b = integer(4))) + a %% b + } + expect_error(quick(modulo), "equal lengths") +}) + +test_that("symbolic differing lengths get a runtime guard", { + fn <- function(a, b) { + declare(type(a = double(n)), type(b = double(m))) + a + b + } + fsub <- as.character(r2f(fn)) + expect_match(fsub, "size(a, kind=c_ptrdiff_t)", fixed = TRUE) + expect_match(fsub, "size(b, kind=c_ptrdiff_t)", fixed = TRUE) + qfn <- quick(fn) + expect_identical(qfn(c(1, 2), c(10, 20)), c(11, 22)) + # was: silent truncation to c(11, 22) + expect_error(qfn(c(1, 2), c(10, 20, 30, 40)), "equal lengths") + # Runtime length one does not change an assumed-shape vector into a scalar. + expect_error(qfn(c(1, 2, 3), 10), "equal lengths") + expect_error(qfn(10, c(1, 2, 3)), "equal lengths") +}) + +test_that("identical symbolic lengths stay guard-free and work", { + fn <- function(a, b) { + declare(type(a = double(n)), type(b = double(n))) + a - b + } + fsub <- r2f(fn) + expect_no_match(fsub, "quickr_set_error_msg", fixed = TRUE) + expect_quick_identical(fn, list(c(1, 2, 3), c(10, 20, 30))) +}) + +test_that("scalar broadcast is unaffected", { + fn <- function(a, b) { + declare(type(a = double(n)), type(b = double(1))) + a + b + } + fsub <- r2f(fn) + expect_no_match(fsub, "quickr_set_error_msg", fixed = TRUE) + expect_quick_identical(fn, list(c(1, 2, 3), 10)) +}) + +test_that("matrix-matrix elementwise ops guard unknown dims per axis", { + fn <- function(a, b) { + declare(type(a = double(n, k)), type(b = double(m, j))) + a * b + } + qfn <- quick(fn) + m1 <- matrix(as.double(1:6), 2, 3) + m2 <- matrix(as.double(6:1), 2, 3) + expect_identical(qfn(m1, m2), m1 * m2) + expect_error(qfn(m1, t(m2)), "matching dimensions") +}) + +test_that("vector-matrix ops with unknown dims guard instead of rejecting", { + fn <- function(vec, mat) { + declare(type(vec = double(n)), type(mat = double(m, k))) + vec + mat + } + qfn <- quick(fn) + mat <- matrix(as.double(1:6), 2, 3) + vec <- c(10, 20) + expect_identical(qfn(vec, mat), vec + mat) + expect_error(qfn(c(10, 20, 30), mat), "matrix first dimension") +}) + +test_that("1x1 matrix operands follow R: arithmetic scalarizes, strict ops reject", { + # Arithmetic: R recycles a length-1 array against a longer vector + # (deprecated, hence suppressWarnings, but still R's answer). A 1x1 + # operand that needs a cast used to emit unindexable expression text + # (`real(b, kind=c_double)(1, 1)`), a gfortran syntax error; it is now + # hoisted to a temporary before subscripting. + cast_fn <- function(a, b) { + declare(type(a = double(3)), type(b = logical(1, 1))) + a + b + } + qfn <- quick(cast_fn) + a <- c(1.5, 2.5, 3.5) + b <- matrix(TRUE) + expect_identical(qfn(a, b), suppressWarnings(cast_fn(a, b))) + + div_fn <- function(a, b) { + declare(type(a = double(3)), type(b = logical(1, 1))) + a / b + } + qdiv <- quick(div_fn) + expect_identical(qdiv(a, b), suppressWarnings(div_fn(a, b))) + + # Comparisons and & | do not get R's length-1 array recycling: R errors + # ("dims [product 1] do not match the length of object"). Scalarizing + # here would answer where R refuses, so the 1x1 is treated as an + # ordinary one-row matrix and rejected. + cmp_fn <- function(a, b) { + declare(type(a = double(3)), type(b = double(1, 1))) + a < b + } + expect_error(quick(cmp_fn), "matrix first dimension") + + and_fn <- function(a, b) { + declare(type(a = logical(3)), type(b = logical(1, 1))) + a & b + } + expect_error(quick(and_fn), "matrix first dimension") + + # Unknown vector length against a 1x1: strict ops guard at runtime + # (length 1 conforms, like R; anything longer is the R error above) + sym_cmp <- function(a, b) { + declare(type(a = double(NA)), type(b = double(1, 1))) + a < b + } + qcmp <- quick(sym_cmp) + expect_identical(qcmp(3, matrix(5)), 3 < matrix(5)) + expect_error(qcmp(c(1, 2, 3), matrix(5)), "matrix first dimension") +}) + +test_that("1x1 matrix with a symbolic-length vector keeps R's shape", { + # The result's shape depends on the runtime length: R keeps the 1x1 + # dims for a length-1 vector and drops them for any other length, so no + # static decision can be right for both. Scalarizing regardless (the + # old behavior) silently returned a dimensionless vector where R + # returns a 1x1 matrix. Symbolic lengths now take the vector-matrix + # rule instead: a runtime guard requires length 1 and the result is a + # 1x1 matrix; longer vectors error where R would recycle (deprecated). + fn <- function(m, x) { + declare(type(m = double(1, 1)), type(x = double(n))) + m + x + } + qfn <- quick(fn) + expect_identical(qfn(matrix(2), 3), fn(matrix(2), 3)) + expect_error(qfn(matrix(2), c(1, 2, 3)), "matrix first dimension") + + rev_fn <- function(x, m) { + declare(type(x = double(n)), type(m = double(1, 1))) + x + m + } + qrev <- quick(rev_fn) + expect_identical(qrev(3, matrix(2)), rev_fn(3, matrix(2))) + expect_error(qrev(c(1, 2, 3), matrix(2)), "matrix first dimension") +}) + +test_that("fill constructors spread inside c()", { + known <- function(x) { + declare(type(x = double(3))) + c(numeric(2), x) + } + expect_quick_identical(known, list(as.double(1:3))) + + symbolic <- function(x, k) { + declare(type(x = double(3)), type(k = integer(1))) + c(numeric(k), x) + } + expect_quick_identical(symbolic, list(as.double(1:3), 2L)) + expect_quick_identical(symbolic, list(as.double(1:3), 0L)) + + promoted <- function(x) { + declare(type(x = double(1))) + c(integer(2), x) + } + expect_quick_identical(promoted, list(1.5)) + + logical_fill <- function(x) { + declare(type(x = logical(2))) + c(logical(3), x) + } + expect_quick_identical(logical_fill, list(c(TRUE, FALSE))) +}) + +test_that("symbolic fill spreading preserves pointer-sized lengths", { + fn <- function(x) { + declare(type(x = double(NA))) + c(numeric(length(x)), 1) + } + fsub <- as.character(r2f(fn)) + expect_match(fsub, "integer(c_ptrdiff_t) :: tmp1_", fixed = TRUE) + expect_match( + fsub, + "tmp1_=1_c_ptrdiff_t, int(x__len_, kind=c_ptrdiff_t)", + fixed = TRUE + ) + expect_quick_identical(fn, list(c(2, 4, 6))) +}) + +test_that("local closures can shadow fill constructors in c() and array()", { + numeric_shadow <- function() { + numeric <- function() c(1, 2) + combined <- c(numeric(), 3) + reshaped <- array(numeric(), dim = c(1L, 2L)) + list(combined = combined, reshaped = reshaped) + } + expect_quick_identical(numeric_shadow, list()) + + integer_shadow <- function() { + integer <- function() c(1L, 2L) + combined <- c(integer(), 3L) + reshaped <- array(integer(), dim = c(1L, 2L)) + list(combined = combined, reshaped = reshaped) + } + expect_quick_identical(integer_shadow, list()) + + double_shadow <- function() { + double <- function() c(1, 2) + combined <- c(double(), 3) + reshaped <- array(double(), dim = c(1L, 2L)) + list(combined = combined, reshaped = reshaped) + } + expect_quick_identical(double_shadow, list()) + + logical_shadow <- function() { + logical <- function() c(1L, 2L) + combined <- c(logical(), 3L) + reshaped <- array(logical(), dim = c(1L, 2L)) + list(combined = combined, reshaped = reshaped) + } + expect_quick_identical(logical_shadow, list()) +}) + +test_that("fill constructors materialize where an array is required", { + # A fill reaching c() through an expression is a real array, not a + # scalar literal with claimed dims (which emitted one element where the + # length arithmetic counted two). + through_op <- function(x) { + declare(type(x = double(2))) + c(numeric(2) + 1, x) + } + expect_quick_identical(through_op, list(c(5, 6))) + + # Same leak as a silent wrong answer: sum() over a fill expression saw + # one scalar instead of the filled length. + reduced <- function() { + sum(numeric(2) + 3) + } + expect_quick_identical(reduced, list()) + + symbolic <- function(x, k) { + declare(type(x = double(2)), type(k = integer(1))) + c(integer(k) + 1L, x) + } + expect_quick_identical(symbolic, list(c(5, 6), 3L)) +}) + +test_that("fill constructors materialize inside matrix()", { + # matrix() lowers non-scalar data through reshape(), whose SOURCE must + # be an array. Fills used to pass through as scalar literals with + # claimed dims and relied on hoist_unless_name() to materialize them; + # once that helper learned to skip literals, the generated + # reshape(0.0_c_double, ...) failed to compile (and logical(k) only + # kept working because the literal regex missed `.false.`). Fills now + # materialize before matrix() like any other array consumer. + numeric_fill <- function() { + matrix(numeric(6), 3, 2) + } + expect_quick_identical(numeric_fill, list()) + + integer_fill <- function() { + matrix(integer(6), 3, 2) + } + expect_quick_identical(integer_fill, list()) + + logical_fill <- function() { + matrix(logical(6), 3, 2) + } + expect_quick_identical(logical_fill, list()) + + assigned <- function() { + x <- matrix(numeric(6), 3, 2) + x + } + expect_quick_identical(assigned, list()) +}) + +test_that("matrix(scalar, m, n) materializes where an array is required", { + reduced <- function() { + sum(matrix(2, 2, 3)) + } + expect_quick_identical(reduced, list()) + + transposed <- function() { + t(matrix(1, 2, 3)) + } + expect_quick_identical(transposed, list()) +}) + +test_that("matrix() materializes direct non-scalar fill constructors", { + fn <- function() { + matrix(numeric(2), 2, 2) + } + expect_quick_identical(fn, list()) +}) + +test_that("a closure's return expression materializes fills and matrix()", { + # A local closure's return expression is compiled on its own, with no + # enclosing call: the materialization decision sees an empty call stack, + # so nothing is broadcasting, spreading, or padding the scalar-with-dims + # form and it has to become a real array. + fill <- function(x) { + declare(type(x = double(3))) + zeros <- function() numeric(3) + x + zeros() + } + expect_quick_identical(fill, list(c(1, 2, 3))) + + mat <- function(x) { + declare(type(x = double(2, 2))) + ones <- function() matrix(1, 2, 2) + x + ones() + } + expect_quick_identical(mat, list(matrix(as.double(1:4), 2, 2))) +}) + +test_that("matrix(scalar, m, n) keeps the broadcast fast path on assignment", { + fn <- function(n, k) { + declare(type(n = integer(1)), type(k = integer(1))) + m <- matrix(0, n, k) + m + } + fsub <- r2f(fn) + # no hoisted temp: the scalar broadcasts straight into the target + expect_match(fsub, "m = 0.0_c_double", fixed = TRUE) + expect_quick_identical(fn, list(2L, 3L)) +}) + +test_that("matrix(scalar, m, n) broadcasts natively in elementwise ops", { + # Against a genuine rank-2 array the fill compiles to its scalar -- + # no O(m*n) temporary is materialized. + broadcast <- function(x, n) { + declare(type(x = double(n, n)), type(n = integer(1))) + x + matrix(1, n, n) + } + expect_false(grepl("allocate", r2f(broadcast), fixed = TRUE)) + expect_quick_identical(broadcast, list(matrix(as.double(1:4), 2, 2), 2L)) + + scalar_var_data <- function(x, s, n) { + declare(type(x = double(n, n)), type(s = double(1)), type(n = integer(1))) + x * matrix(s, n, n) + } + expect_quick_identical( + scalar_var_data, + list(matrix(as.double(1:4), 2, 2), 3, 2L) + ) + + # The claimed dims still participate in the conformability contract. + static_mismatch <- function(x) { + declare(type(x = double(2, 2))) + x + matrix(1, 3, 3) + } + expect_error(quick(static_mismatch), "matching dimensions") + + symbolic <- function(x, k) { + declare(type(x = double(2, 2)), type(k = integer(1))) + x + matrix(1, k, k) + } + q_symbolic <- quick(symbolic) + expect_error( + q_symbolic(matrix(as.double(1:4), 2, 2), 3L), + "matching dimensions" + ) + expect_identical( + q_symbolic(matrix(as.double(1:4), 2, 2), 2L), + symbolic(matrix(as.double(1:4), 2, 2), 2L) + ) + + # Two fills meeting each other still materialize (no scalar result + # with claimed array dims may escape). + both_fills <- function(n) { + declare(type(n = integer(1))) + sum(matrix(2, n, n) + matrix(3, n, n)) + } + expect_quick_identical(both_fills, list(2L)) + + # A vector operand keeps the vector-matrix reshape rule. + vec_operand <- function(v) { + declare(type(v = double(2))) + v + matrix(1, 2, 3) + } + expect_quick_identical(vec_operand, list(c(1, 2))) +}) diff --git a/tests/testthat/test-size-constraint.R b/tests/testthat/test-size-constraint.R index 7b42fff..7e5e553 100644 --- a/tests/testthat/test-size-constraint.R +++ b/tests/testthat/test-size-constraint.R @@ -5,7 +5,7 @@ skip_on_cran() test_that("size constraint", { fn <- function(a, b) { declare(type(a = double(n)), type(b = double(n + 1))) - a <- sum(b) + a <- a + sum(b) a } @@ -20,7 +20,9 @@ test_that("size constraint", { fixed = TRUE ) expect_translation_snapshots(fn, "call_size_constraint") - expect_equal(qfn(1, c(2, 3)), 5) + # `a` stays an array: reassigning a scalar into it would be a shape + # change, which R does by rebinding and Fortran cannot do at all + expect_equal(qfn(1, c(2, 3)), 6) }) test_that("size constraint", { diff --git a/tests/testthat/test-size-expr-abs.R b/tests/testthat/test-size-expr-abs.R index 1027730..c7ad802 100644 --- a/tests/testthat/test-size-expr-abs.R +++ b/tests/testthat/test-size-expr-abs.R @@ -29,9 +29,27 @@ test_that("declare() size expressions validate abs() arity", { type(m = integer(1)), type(out = double(abs(n, m))) ) - out <- double(1L) + out <- double(abs(n, m)) out } expect_error(quick(bad), "unused argument", fixed = TRUE) }) + +test_that("declare() size expressions support as.integer()", { + # as.integer() reaches size expressions through diag()'s identity form, + # but it is spellable on its own: INT() in Fortran, a cast in the bridge + fn <- function(x, n) { + declare(type(x = double(1)), type(n = integer(1))) + out <- double(as.integer(x) + n) + out + } + expect_quick_equal(fn, list(2, 3L), list(2.9, 1L)) + + bad <- function(x) { + declare(type(x = double(1))) + out <- double(as.integer(x, 2L)) + out + } + expect_error(quick(bad), "expects one argument", fixed = TRUE) +}) diff --git a/tests/testthat/test-size-expr-dim-nrow-ncol.R b/tests/testthat/test-size-expr-dim-nrow-ncol.R index 8097c3b..9b2d2db 100644 --- a/tests/testthat/test-size-expr-dim-nrow-ncol.R +++ b/tests/testthat/test-size-expr-dim-nrow-ncol.R @@ -42,12 +42,12 @@ test_that("declare() size expressions support dim(x)[axis] indices", { test_that("declare() size expressions validate nrow()/ncol() arity", { bad_nrow <- function(x) { declare(type(x = double(NA, NA)), type(out = double(nrow(x, 1L)))) - out <- double(1L) + out <- double(nrow(x, 1L)) out } bad_ncol <- function(x) { declare(type(x = double(NA, NA)), type(out = double(ncol(x, 1L)))) - out <- double(1L) + out <- double(ncol(x, 1L)) out } @@ -68,7 +68,7 @@ test_that("declare() size expressions reject ncol() beyond variable rank", { test_that("declare() size expressions reject unsupported calls", { bad <- function(n) { declare(type(n = integer(1)), type(out = double(sum(n)))) - out <- double(1L) + out <- double(sum(n)) out } diff --git a/tests/testthat/test-sizes-arithmetic.R b/tests/testthat/test-sizes-arithmetic.R index bd6c588..9a28247 100644 --- a/tests/testthat/test-sizes-arithmetic.R +++ b/tests/testthat/test-sizes-arithmetic.R @@ -43,6 +43,230 @@ test_that("dynamic exponentiation works in declared dims", { expect_quick_identical(fn, list(3L), list(4L)) }) +test_that("size division keeps double precision until the final cast", { + fn <- function(x, y) { + declare(type(x = double(1)), type(y = double(1))) + double(as.integer(x / y)) + } + + code <- suppressWarnings(r2f(fn)) + expect_match( + as.character(code), + "real(x, kind=c_double) / real(y, kind=c_double)", + fixed = TRUE + ) + expect_match(as.character(code), "kind=c_ptrdiff_t", fixed = TRUE) + expect_match(code@c_bridge, "Rf_asReal(x)", fixed = TRUE) + expect_match(code@c_bridge, "Rf_asReal(y)", fixed = TRUE) + + expect_quick_identical(fn, list(1.99999999, 1)) +}) + +test_that("size integer division evaluates numeric operands before casting", { + fn <- function(x, y) { + declare(type(x = double(1)), type(y = double(1))) + double(x %/% y) + } + + code <- suppressWarnings(r2f(fn)) + expect_match(as.character(code), "aint(", fixed = TRUE) + expect_match(as.character(code), "real(x, kind=c_double)", fixed = TRUE) + expect_match(as.character(code), "real(y, kind=c_double)", fixed = TRUE) + expect_match(code@c_bridge, "floor(", fixed = TRUE) + expect_match(code@c_bridge, "Rf_asReal(x)", fixed = TRUE) + expect_match(code@c_bridge, "Rf_asReal(y)", fixed = TRUE) + + qfn <- suppressWarnings(quick(fn)) + expect_identical(qfn(3.9, 1.9), fn(3.9, 1.9)) +}) + +test_that("size integer division rounds negative quotients down", { + fn <- function(x, y) { + declare(type(x = integer(1)), type(y = integer(1))) + double((x %/% y) + 3L) + } + + expect_quick_identical(fn, list(-3L, 2L)) +}) + +test_that("size floor division remains real before outer arithmetic", { + fn <- function(x, y, z) { + declare( + type(x = double(1)), + type(y = double(1)), + type(z = double(1)) + ) + out <- double(1) + local <- double(as.integer((x %/% y) / z) + 10L) + out[1] <- as.double(length(local)) + out + } + + code <- r2f(fn) + expect_match(as.character(code), "aint(", fixed = TRUE) + expect_quick_identical( + fn, + list(1e20, 3, 1e19), + list(-1e20, 3, 1e19) + ) + + returned <- function(x, y, z) { + declare( + type(x = double(1)), + type(y = double(1)), + type(z = double(1)) + ) + out <- double(as.integer((x %/% y) / z) + 10L) + for (i in seq_len(length(out))) { + out[i] <- as.double(i) + } + out + } + expect_quick_identical( + returned, + list(1e20, 3, 1e19), + list(-1e20, 3, 1e19) + ) +}) + +test_that("size min and max use one numeric domain", { + min_fn <- function(n, x) { + declare( + type(n = integer(1)), + type(x = double(min(n %/% 2L, 5L, 6L))) + ) + sum(x) + } + max_fn <- function(n, x) { + declare( + type(n = integer(1)), + type(x = double(max(n %/% 2L, 5L, 6L))) + ) + sum(x) + } + + min_code <- as.character(r2f(min_fn)) + max_code <- as.character(r2f(max_fn)) + expect_match(min_code, "min(real(", fixed = TRUE) + expect_match(max_code, "max(real(", fixed = TRUE) + for (code in list(min_code, max_code)) { + expect_match(code, "real(5, kind=c_double)", fixed = TRUE) + expect_match(code, "real(6, kind=c_double)", fixed = TRUE) + expect_match(code, "kind=c_ptrdiff_t", fixed = TRUE) + } + + expect_quick_identical(min_fn, list(8L, as.double(1:4))) + expect_quick_identical(max_fn, list(8L, as.double(1:6))) +}) + +test_that("one-argument size min and max are identity operations", { + min_fn <- function(n, x) { + declare(type(n = integer(1)), type(x = double(min(n)))) + sum(x) + } + max_fn <- function(n, x) { + declare(type(n = integer(1)), type(x = double(max(n)))) + sum(x) + } + + expect_quick_identical(min_fn, list(3L, as.double(1:3))) + expect_quick_identical(max_fn, list(4L, as.double(1:4))) +}) + +test_that("zero-argument size min and max fail at translation", { + min_fn <- function(x) { + declare(type(x = double(min()))) + sum(x) + } + max_fn <- function(x) { + declare(type(x = double(max()))) + sum(x) + } + + expect_error( + quick(min_fn), + "min() size expressions require at least one argument", + fixed = TRUE + ) + expect_error( + quick(max_fn), + "max() size expressions require at least one argument", + fixed = TRUE + ) +}) + +test_that("size modulo uses the divisor's sign", { + fn <- function(x, y) { + declare(type(x = integer(1)), type(y = integer(1))) + double((x %% y) + 1L) + } + + code <- r2f(fn) + expect_match(as.character(code), "modulo(", fixed = TRUE) + expect_match(code@c_bridge, "fmod(fmod(", fixed = TRUE) + + expect_quick_identical(fn, list(-3L, 2L)) +}) + +test_that("size modulo avoids cancellation in the C bridge", { + fn <- function(x, y) { + declare(type(x = double(1)), type(y = double(1))) + out <- double(as.integer((x %% y) * 20) + 10L) + for (i in seq_len(length(out))) { + out[i] <- as.double(i) + } + out + } + + code <- r2f(fn) + expect_match(code@c_bridge, "fmod(fmod(", fixed = TRUE) + expect_match(code@c_bridge, "#include ", fixed = TRUE) + + expect_quick_identical(fn, list(1, 0.1), list(1, -0.1)) +}) + +test_that("size powers use the double domain before casting", { + fn <- function(x, y) { + declare(type(x = integer(1)), type(y = integer(1))) + double(as.integer((x^-1L) * y)) + } + + code <- r2f(fn) + expect_match(as.character(code), "real(x, kind=c_double)", fixed = TRUE) + expect_match(as.character(code), "kind=c_ptrdiff_t", fixed = TRUE) + expect_match(code@c_bridge, "R_pow", fixed = TRUE) + + expect_quick_identical(fn, list(2L, 4L)) + + constant <- function() { + double(as.integer((2L^-1L) * 4L)) + } + expect_quick_identical(constant, list()) +}) + +test_that("size powers retain integer exponent type", { + fn <- function(x, exponent, scale) { + declare( + type(x = integer(1)), + type(exponent = integer(1)), + type(scale = integer(1)) + ) + out <- double(as.integer((x^exponent) * scale) + 10L) + for (i in seq_len(length(out))) { + out[i] <- as.double(i) + } + out + } + + code <- r2f(fn) + expect_match(as.character(code), "**(exponent)", fixed = TRUE) + expect_false( + grepl("real(exponent, kind=c_double)", as.character(code), fixed = TRUE) + ) + + expect_quick_identical(fn, list(-2L, -1L, 4L)) +}) + test_that("dim/length/nrow/ncol are supported in allocation sizes", { vec <- function(x) { declare(type(x = double(NA))) diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/quickr-semantics.Rmd b/vignettes/quickr-semantics.Rmd new file mode 100644 index 0000000..24e268a --- /dev/null +++ b/vignettes/quickr-semantics.Rmd @@ -0,0 +1,367 @@ +--- +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. This vignette documents the semantics of +that subset: what type a result has, when shapes are checked, and every +place where a compiled function deliberately behaves differently from R. +Except for the `prod()` result type documented below, all differences +take the same form — an error where R would return a value — never a +different value. + +## The contract + +> `quick(f)(x)` either returns exactly what `f(x)` returns — same values, +> same `typeof()`, same shape — or raises an error. Never a third thing. +> Integer and logical `prod()` results have the documented type divergence +> below. +> The unchecked `NA` inputs and out-of-bounds runtime subscripts documented +> below are outside this contract and have undefined behavior. + +When quickr cannot reproduce R's behavior, it refuses. It prefers to +refuse at compile time, when `quick()` is called; when the decision +depends on values only known at run time (for example, sizes declared as +`NA` or as symbolic constraints like `double(n)`), the compiled function +performs a cheap runtime check and raises an error on mismatch. The text +of these error messages differs from R's. + +The rest of this vignette is that contract in detail: first result +*types*, then operand *shapes*, then the list of deliberate divergences. + +## 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. + +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`) | +| single-argument `min()` / `max()` / `sum()`, `abs()` | type of `x`; logical counts as integer | +| `prod()` | join of the argument types; unlike R, an integer/logical-only product is integer rather than double | +| `ifelse(test, yes, no)` | join of `yes` and `no` (but see [ifelse](#ifelse) below) | +| `%*%`, `crossprod()`, `solve()`, and other linear algebra | always double, as in R; complex operands are a compile error (see [complex values](#complex)) | +| `t(x)`, `diag(x)` | same type as `x`; the identity forms `diag(n)` are double, as in R | +| `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 always double, as in R, but when the +exponent is a whole number quickr computes the power with an integer +exponent. This is exact, and well-defined for negative bases: +`(-2) ^ 3L` is exactly `-8`. + +### 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 +inserts the conversion R would perform. + +## Shapes + +Every operation that combines two or more values checks that their +shapes are compatible. A shape question has one of three answers, and +each answer has a fixed consequence: + +- **Provably compatible** at compile time — allowed, no check emitted. +- **Provably incompatible** at compile time — compile error. +- **Unknown** at compile time (sizes declared `NA`, or symbolic sizes + that cannot be proven equal) — the compiled function checks at run + time and raises an error on mismatch. + +There is no case where a mismatch is silently accepted. + +For elementwise operations (arithmetic, comparisons, `&`, `|`), +"compatible" means: + +| Operands | Behavior | +|---|---| +| scalar with anything | allowed; the scalar is broadcast | +| identical 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 other 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 the one place 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. A compiled function's result shape cannot depend on a +value, 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. + +The last row is the important one. In R, `1:6 + c(10, 20)` recycles the +shorter vector. quickr never does this: + +```{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 +``` + +The two recycling forms that quickr *does* support — scalars, and a +vector spanning the rows of a matrix — are exactly the ones with an +exact, non-partial interpretation: + +```{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. + +## Differences from R + +Everything listed here is an *error* divergence: a compiled function +refuses where plain R would produce a value. If you find a case where a +compiled function silently returns something different from R, that is a +bug — please report it. + +### 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 arithmetic per IEEE rules as they do in R, +but no guarantees are made for code that branches on them.) + +### 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. + +### 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. An +operand *known* at compile time to be zero-length combines only with a +scalar (`double(0) + 1` is `numeric(0)`, matching R); combining it with +any other vector — even another known-zero-length one — is a compile +error. When lengths are only known at run time, a zero-length operand +errors unless the other operand's length is also zero (two zero-length +operands produce a zero-length result, as in R). + +### 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. + +### `prod()` keeps integer products + +quickr preserves the joined input type for `prod()`. A product whose +inputs are all integer or logical therefore has integer type. R instead +returns double from `prod()` even when every input is integer or logical. + +### Complex values are elementwise-only {#complex} + +Elementwise arithmetic and equality comparisons handle complex values +natively, including mixed operands: `complex + double` promotes to +complex, as in R. Everything else refuses: + +- **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. +- **Linear algebra.** `%*%`, `crossprod()`, `solve()`, `chol()`, and the + rest of the linear-algebra surface refuse complex operands at compile + time; quickr's lowerings are double-only, where R computes complex + results. + +(`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.) + +### Subscript bounds are only partially checked + +R validates every subscript: `x[0]` drops elements, negative subscripts +exclude, and out-of-range reads pad with `NA`. None of those have a +Fortran equivalent, and per-element bounds checks inside hot loops would +defeat the purpose of compiling, so quickr's contract is: + +- 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. + +If you need R's bounds behavior, validate indices before the loop. + +### `ifelse()` {#ifelse} + +Three differences: + +- **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. + +```{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`); quickr +requires the operands of `&`, `|`, `&&`, and `||` to already be logical +and errors otherwise. + +`&&` and `||` otherwise follow R's scalar semantics: operands must be +length 1 (a compile error in quickr; R raises the equivalent error at +run time), 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 <= n && x[i] > 0` never evaluates `x[i]` out of range. For +elementwise logic over vectors, use `&` and `|`. + +### Closure arguments are evaluated eagerly + +R evaluates function arguments lazily. Local closures inside compiled +functions (including functions passed to `sapply()`) evaluate their +arguments eagerly. Code that relies on an argument never being evaluated +will behave differently. + +### Deprecation warnings are not reproduced + +Where R accepts an operation but warns — recycling a `1x1` matrix in +arithmetic is the one such case in the supported subset — quickr +performs the operation without a warning.