diff --git a/NAMESPACE b/NAMESPACE index 709c792d1..ca284d3d8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -582,6 +582,8 @@ export(vec_math_base) export(vec_names) export(vec_names2) export(vec_order) +export(vec_pall) +export(vec_pany) export(vec_proxy) export(vec_proxy_compare) export(vec_proxy_equal) diff --git a/NEWS.md b/NEWS.md index d1902e047..0e6fe32a3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # vctrs (development version) +* New `vec_pany()` and `vec_pall()`, parallel variants of `any()` and `all()` (in the same way that `pmin()` and `pmax()` are parallel variants of `min()` and `max()`). + * The deprecated C callable for `vec_is_vector()` has been removed. * Fixed the C level signature for the `exp_short_init_compact_seq()` callable. diff --git a/R/parallel.R b/R/parallel.R new file mode 100644 index 000000000..db9892a4f --- /dev/null +++ b/R/parallel.R @@ -0,0 +1,104 @@ +#' Parallel `any()` and `all()` +#' +#' @description +#' These functions are variants of [any()] and [all()] that work in parallel on +#' multiple inputs at once. They work similarly to how [pmin()] and [pmax()] are +#' parallel variants of [min()] and [max()]. +#' +#' @inheritParams rlang::args_error_context +#' +#' @param ... Logical vectors of equal size. +#' +#' @param .missing Value to use when a missing value is encountered. One of: +#' +#' - `NA` to propagate missing values. With this, missings are treated the +#' same way as `|` or `&`. +#' +#' - `FALSE` to treat missing values as `FALSE`. +#' +#' - `TRUE` to treat missing values as `TRUE`. +#' +#' @param .size An optional output size. Only useful to specify if it is possible +#' for no inputs to be provided. +#' +#' @param .arg Argument name used in error messages. +#' +#' @returns A logical vector the same size as the vectors in `...`. +#' +#' @details +#' `vec_pany()` and `vec_pall()` are consistent with [any()] and [all()] when +#' there are no inputs to process in parallel: +#' +#' - `any()` returns `FALSE` with no inputs. Similarly, `vec_pany(.size = 1)` +#' returns `FALSE`. +#' +#' - `all()` returns `TRUE` with no inputs. Similarly, `vec_pall(.size = 1)` +#' returns `TRUE`. +#' +#' @name parallel-operators +#' +#' @examples +#' a <- c(TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, NA, NA, NA) +#' b <- c(TRUE, FALSE, NA, TRUE, FALSE, NA, TRUE, FALSE, NA) +#' +#' # Default behavior treats missings like `|` does +#' vec_pany(a, b) +#' a | b +#' +#' # Default behavior treats missings like `&` does +#' vec_pall(a, b) +#' a & b +#' +#' # Remove missings from the computation, like `na_rm = TRUE` +#' vec_pany(a, b, .missing = FALSE) +#' (a & !is.na(a)) | (b & !is.na(b)) +#' +#' vec_pall(a, b, .missing = TRUE) +#' (a | is.na(a)) & (b | is.na(b)) +#' +#' # `vec_pall()` can be used to implement a `dplyr::filter()` style API +#' df <- data_frame(id = seq_along(a), a = a, b = b) +#' +#' keep_rows <- function(x, ...) { +#' vec_slice(x, vec_pall(..., .missing = FALSE)) +#' } +#' drop_rows <- function(x, ...) { +#' vec_slice(x, !vec_pall(..., .missing = FALSE)) +#' } +#' +#' # "Keep / Drop the rows when both a and b are TRUE" +#' # These form complements of one another, even with `NA`s. +#' keep_rows(df, a, b) +#' drop_rows(df, a, b) +#' +#' # Same empty behavior as `any()` and `all()` +#' vec_pany(.size = 1) +#' any() +#' +#' vec_pall(.size = 1) +#' all() +NULL + +#' @rdname parallel-operators +#' @export +vec_pany <- function( + ..., + .missing = NA, + .size = NULL, + .arg = "", + .error_call = current_env() +) { + .Call(ffi_vec_pany, list2(...), .missing, .size, environment()) +} + +#' @rdname parallel-operators +#' @export +vec_pall <- function( + ..., + .missing = NA, + .size = NULL, + .arg = "", + .error_call = current_env() +) { + .Call(ffi_vec_pall, list2(...), .missing, .size, environment()) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 61ffa4594..928108779 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -103,6 +103,10 @@ reference: - vec_identify_runs - vec_expand_grid +- title: Reducers + contents: + - vec_pall + - title: New classes contents: - list_of diff --git a/man/parallel-operators.Rd b/man/parallel-operators.Rd new file mode 100644 index 000000000..0b8419fe7 --- /dev/null +++ b/man/parallel-operators.Rd @@ -0,0 +1,104 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/parallel.R +\name{parallel-operators} +\alias{parallel-operators} +\alias{vec_pany} +\alias{vec_pall} +\title{Parallel \code{any()} and \code{all()}} +\usage{ +vec_pany( + ..., + .missing = NA, + .size = NULL, + .arg = "", + .error_call = current_env() +) + +vec_pall( + ..., + .missing = NA, + .size = NULL, + .arg = "", + .error_call = current_env() +) +} +\arguments{ +\item{...}{Logical vectors of equal size.} + +\item{.missing}{Value to use when a missing value is encountered. One of: +\itemize{ +\item \code{NA} to propagate missing values. With this, missings are treated the +same way as \code{|} or \code{&}. +\item \code{FALSE} to treat missing values as \code{FALSE}. +\item \code{TRUE} to treat missing values as \code{TRUE}. +}} + +\item{.size}{An optional output size. Only useful to specify if it is possible +for no inputs to be provided.} + +\item{.arg}{Argument name used in error messages.} + +\item{.error_call}{The execution environment of a currently +running function, e.g. \code{caller_env()}. The function will be +mentioned in error messages as the source of the error. See the +\code{call} argument of \code{\link[rlang:abort]{abort()}} for more information.} +} +\value{ +A logical vector the same size as the vectors in \code{...}. +} +\description{ +These functions are variants of \code{\link[=any]{any()}} and \code{\link[=all]{all()}} that work in parallel on +multiple inputs at once. They work similarly to how \code{\link[=pmin]{pmin()}} and \code{\link[=pmax]{pmax()}} are +parallel variants of \code{\link[=min]{min()}} and \code{\link[=max]{max()}}. +} +\details{ +\code{vec_pany()} and \code{vec_pall()} are consistent with \code{\link[=any]{any()}} and \code{\link[=all]{all()}} when +there are no inputs to process in parallel: +\itemize{ +\item \code{any()} returns \code{FALSE} with no inputs. Similarly, \code{vec_pany(.size = 1)} +returns \code{FALSE}. +\item \code{all()} returns \code{TRUE} with no inputs. Similarly, \code{vec_pall(.size = 1)} +returns \code{TRUE}. +} +} +\examples{ +a <- c(TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, NA, NA, NA) +b <- c(TRUE, FALSE, NA, TRUE, FALSE, NA, TRUE, FALSE, NA) + +# Default behavior treats missings like `|` does +vec_pany(a, b) +a | b + +# Default behavior treats missings like `&` does +vec_pall(a, b) +a & b + +# Remove missings from the computation, like `na_rm = TRUE` +vec_pany(a, b, .missing = FALSE) +(a & !is.na(a)) | (b & !is.na(b)) + +vec_pall(a, b, .missing = TRUE) +(a | is.na(a)) & (b | is.na(b)) + +# `vec_pall()` can be used to implement a `dplyr::filter()` style API +df <- data_frame(id = seq_along(a), a = a, b = b) + +keep_rows <- function(x, ...) { + vec_slice(x, vec_pall(..., .missing = FALSE)) +} +drop_rows <- function(x, ...) { + vec_slice(x, !vec_pall(..., .missing = FALSE)) +} + +# "Keep / Drop the rows when both a and b are TRUE" +# These form complements of one another, even with `NA`s. +keep_rows(df, a, b) +drop_rows(df, a, b) + +# Same empty behavior as `any()` and `all()` +vec_pany(.size = 1) +any() + +vec_pall(.size = 1) +all() +} diff --git a/src/decl/parallel-decl.h b/src/decl/parallel-decl.h new file mode 100644 index 000000000..3122a4d71 --- /dev/null +++ b/src/decl/parallel-decl.h @@ -0,0 +1,41 @@ +static +r_obj* vec_parallel( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call, + enum vec_parallel_variant parallel +); + +static inline +void vec_parallel_init(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out); +static inline +void vec_parallel_init_missing_as_na(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_parallel_init_missing_as_false(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_parallel_init_missing_as_true(const int* v_x, r_ssize size, int* v_out); + +static inline +void vec_pany_fill(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out); +static inline +void vec_pall_fill(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out); +static inline +void vec_pany_fill_missing_as_na(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_pall_fill_missing_as_na(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_pany_fill_missing_as_false(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_pall_fill_missing_as_false(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_pany_fill_missing_as_true(const int* v_x, r_ssize size, int* v_out); +static inline +void vec_pall_fill_missing_as_true(const int* v_x, r_ssize size, int* v_out); + +static +enum vec_parallel_missing parse_vec_parallel_missing(r_obj* missing, struct r_lazy error_call); + +static +r_ssize compute_size(r_ssize size, r_obj* xs); diff --git a/src/init.c b/src/init.c index 64d03f730..4a1b7a0df 100644 --- a/src/init.c +++ b/src/init.c @@ -171,6 +171,8 @@ extern r_obj* ffi_vec_replace_when(r_obj*, r_obj*, r_obj*, r_obj*); extern r_obj* ffi_vec_recode_values(r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*); extern r_obj* ffi_vec_replace_values(r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*); extern r_obj* ffi_vec_if_else(r_obj*, r_obj*, r_obj*, r_obj*, r_obj*, r_obj*); +extern r_obj* ffi_vec_pany(r_obj*, r_obj*, r_obj*, r_obj*); +extern r_obj* ffi_vec_pall(r_obj*, r_obj*, r_obj*, r_obj*); // Maturing @@ -372,6 +374,8 @@ static const R_CallMethodDef CallEntries[] = { {"ffi_vec_recode_values", (DL_FUNC) &ffi_vec_recode_values, 9}, {"ffi_vec_replace_values", (DL_FUNC) &ffi_vec_replace_values, 6}, {"ffi_vec_if_else", (DL_FUNC) &ffi_vec_if_else, 6}, + {"ffi_vec_pany", (DL_FUNC) &ffi_vec_pany, 4}, + {"ffi_vec_pall", (DL_FUNC) &ffi_vec_pall, 4}, {"ffi_exp_vec_cast", (DL_FUNC) &exp_vec_cast, 2}, {NULL, NULL, 0} }; diff --git a/src/parallel.c b/src/parallel.c new file mode 100644 index 000000000..1ad903425 --- /dev/null +++ b/src/parallel.c @@ -0,0 +1,369 @@ +#include "parallel.h" +#include "assert.h" +#include "slice-assign.h" + +enum vec_parallel_variant { + VEC_PARALLEL_VARIANT_all, + VEC_PARALLEL_VARIANT_any +}; + +#include "decl/parallel-decl.h" + +r_obj* ffi_vec_pany(r_obj* ffi_xs, r_obj* ffi_missing, r_obj* ffi_size, r_obj* ffi_frame) { + struct r_lazy xs_arg_lazy = { .x = syms.dot_arg, .env = ffi_frame }; + struct vctrs_arg xs_arg = new_lazy_arg(&xs_arg_lazy); + + const struct r_lazy error_call = { .x = syms.dot_error_call, .env = ffi_frame }; + + const enum vec_parallel_missing missing = parse_vec_parallel_missing(ffi_missing, error_call); + const r_ssize size = (ffi_size == r_null) ? -1 : r_arg_as_ssize(ffi_size, ".size"); + + return vec_pany(ffi_xs, missing, size, &xs_arg, error_call); +} + +r_obj* ffi_vec_pall(r_obj* ffi_xs, r_obj* ffi_missing, r_obj* ffi_size, r_obj* ffi_frame) { + struct r_lazy xs_arg_lazy = { .x = syms.dot_arg, .env = ffi_frame }; + struct vctrs_arg xs_arg = new_lazy_arg(&xs_arg_lazy); + + const struct r_lazy error_call = { .x = syms.dot_error_call, .env = ffi_frame }; + + const enum vec_parallel_missing missing = parse_vec_parallel_missing(ffi_missing, error_call); + const r_ssize size = (ffi_size == r_null) ? -1 : r_arg_as_ssize(ffi_size, ".size"); + + return vec_pall(ffi_xs, missing, size, &xs_arg, error_call); +} + +r_obj* vec_pany( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call +) { + return vec_parallel(xs, missing, size, p_xs_arg, error_call, VEC_PARALLEL_VARIANT_any); +} + +r_obj* vec_pall( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call +) { + return vec_parallel(xs, missing, size, p_xs_arg, error_call, VEC_PARALLEL_VARIANT_all); +} + +static +r_obj* vec_parallel( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call, + enum vec_parallel_variant parallel +) { + // Input must be a list + obj_check_list(xs, p_xs_arg, error_call); + + // Every element of that list must be a bare logical vector + list_check_all_condition_indices(xs, p_xs_arg, error_call); + + // Every element of that list must be the same size + size = compute_size(size, xs); + list_check_all_size(xs, size, VCTRS_ALLOW_NULL_no, p_xs_arg, error_call); + + r_obj* out = KEEP(r_alloc_logical(size)); + int* v_out = r_lgl_begin(out); + + const r_ssize xs_size = r_length(xs); + r_obj* const* v_xs = r_list_cbegin(xs); + + if (xs_size == 0) { + // Zero input case is special, fill with values that match `any()` and `all()` + switch (parallel) { + case VEC_PARALLEL_VARIANT_all: r_p_lgl_fill(v_out, 1, size); break; + case VEC_PARALLEL_VARIANT_any: r_p_lgl_fill(v_out, 0, size); break; + default: r_stop_unreachable(); + } + } else { + // Initialize output with first input + r_obj* x = v_xs[0]; + const int* v_x = r_lgl_begin(x); + vec_parallel_init(v_x, missing, size, v_out); + + // Combine with remaining inputs + for (r_ssize i = 1; i < xs_size; ++i) { + r_obj* x = v_xs[i]; + const int* v_x = r_lgl_begin(x); + + switch (parallel) { + case VEC_PARALLEL_VARIANT_all: vec_pall_fill(v_x, missing, size, v_out); break; + case VEC_PARALLEL_VARIANT_any: vec_pany_fill(v_x, missing, size, v_out); break; + default: r_stop_unreachable(); + } + } + } + + FREE(1); + return out; +} + +// ----------------------------------------------------------------------------- + +// Same, regardless of variant +static inline +void vec_parallel_init(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out) { + switch (missing) { + case VEC_PARALLEL_MISSING_na: vec_parallel_init_missing_as_na(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_false: vec_parallel_init_missing_as_false(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_true: vec_parallel_init_missing_as_true(v_x, size, v_out); break; + default: r_stop_unreachable(); + } +} + +// Propagates `NA` +static inline +void vec_parallel_init_missing_as_na(const int* v_x, r_ssize size, int* v_out) { + r_memcpy(v_out, v_x, sizeof(*v_out) * size); +} + +// Turns `NA` into `FALSE` +static inline +void vec_parallel_init_missing_as_false(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt = v_x[i]; + v_out[i] = (elt != r_globals.na_lgl) * elt; + } +} + +// Turns `NA` into `TRUE` +static inline +void vec_parallel_init_missing_as_true(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + v_out[i] = (bool) v_x[i]; + } +} + +// ----------------------------------------------------------------------------- + +static inline +void vec_pany_fill(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out) { + switch (missing) { + case VEC_PARALLEL_MISSING_na: vec_pany_fill_missing_as_na(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_false: vec_pany_fill_missing_as_false(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_true: vec_pany_fill_missing_as_true(v_x, size, v_out); break; + default: r_stop_unreachable(); + } +} + +static inline +void vec_pall_fill(const int* v_x, enum vec_parallel_missing missing, r_ssize size, int* v_out) { + switch (missing) { + case VEC_PARALLEL_MISSING_na: vec_pall_fill_missing_as_na(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_false: vec_pall_fill_missing_as_false(v_x, size, v_out); break; + case VEC_PARALLEL_MISSING_true: vec_pall_fill_missing_as_true(v_x, size, v_out); break; + default: r_stop_unreachable(); + } +} + +/* + * Each of these implementations has been highly optimized to be completely + * branchless. Additionally, we are careful to ensure that the access of both + * `v_out[i]` and `v_x[i]` is mandatory at each iteration rather than + * conditional (i.e. `v_out[i] && v_x[i]` vs `elt_out && elt_x`). Conditional + * access of `v_x[i]` in particular can destroy performance here, as it prevents + * the compiler from heavily optimizing the actual computation. + * + * Additionally, the implementations of pall/pany have been designed to be as + * symmetrical as possible to increase code clarity. For example, + * `vec_pall_fill_*()` and `vec_pany_fill_*()` are symmetrical. + * + * A nice property of these implementations is that they don't rely on + * assumptions about two's complement, bitwise operations, or the underlying + * value of `NA_LOGICAL` in any way, making them as portable as possible. + */ + +/* + * F || F == F + * F || T == T + * F || N == N + * + * T || F == T + * T || T == T + * T || N == T + * + * N || F == N + * N || T == T + * N || N == N + */ +static inline +void vec_pany_fill_missing_as_na(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + + const bool any_true = (elt_out == 1) || (elt_x == 1); + const bool equal = elt_out == elt_x; + v_out[i] = any_true + !any_true * (equal * elt_out + !equal * r_globals.na_lgl); + } +} + +/* + * F && F == F + * F && T == F + * F && N == F + * + * T && F == F + * T && T == T + * T && N == N + * + * N && F == F + * N && T == N + * N && N == N + */ +static inline +void vec_pall_fill_missing_as_na(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + + const bool any_false = !elt_out || !elt_x; + const bool equal = elt_out == elt_x; + v_out[i] = !any_false * (equal * elt_out + !equal * r_globals.na_lgl); + } +} + +/* + * Never need to worry about `N || *`, because the initialization loop + * turns the first input's `N`s into `F`s. + * + * Treat `N == F` + * + * F || F == F + * F || T == T + * F || N == F + * + * T || F == T + * T || T == T + * T || N == T + */ +static inline +void vec_pany_fill_missing_as_false(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + v_out[i] = elt_out || (elt_x == 1); + } +} + +/* + * Never need to worry about `N && *`, because the initialization loop + * turns the first input's `N`s into `F`s. + * + * Treat `N == F` + * + * F && F == F + * F && T == F + * F && N == F + * + * T && F == F + * T && T == T + * T && N == F + */ +static inline +void vec_pall_fill_missing_as_false(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + v_out[i] = elt_out && (elt_x == 1); + } +} + +/* + * Never need to worry about `N || *`, because the initialization loop + * turns the first input's `N`s into `T`s. + * + * Treat `N == T` + * + * F || F == F + * F || T == T + * F || N == T + * + * T || F == T + * T || T == T + * T || N == T + */ +static inline +void vec_pany_fill_missing_as_true(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + v_out[i] = elt_out || elt_x; + } +} + +/* + * Never need to worry about `N && *`, because the initialization loop + * turns the first input's `N`s into `T`s. + * + * Treat `N == T` + * + * F && F == F + * F && T == F + * F && N == F + * + * T && F == F + * T && T == T + * T && N == T + */ +static inline +void vec_pall_fill_missing_as_true(const int* v_x, r_ssize size, int* v_out) { + for (r_ssize i = 0; i < size; ++i) { + const int elt_out = v_out[i]; + const int elt_x = v_x[i]; + v_out[i] = elt_out && elt_x; + } +} + +// ----------------------------------------------------------------------------- + +static +bool r_is_scalar_logical(r_obj* x) { + return r_typeof(x) == R_TYPE_logical && r_length(x) == 1; +} + +static +enum vec_parallel_missing parse_vec_parallel_missing(r_obj* missing, struct r_lazy error_call) { + if (!r_is_scalar_logical(missing)) { + r_abort_lazy_call(error_call, "`.missing` must be `NA`, `FALSE`, or `TRUE`."); + } + + const int c_missing = r_lgl_get(missing, 0); + + if (c_missing == r_globals.na_lgl) { + return VEC_PARALLEL_MISSING_na; + } else if (c_missing == 0) { + return VEC_PARALLEL_MISSING_false; + } else if (c_missing == 1) { + return VEC_PARALLEL_MISSING_true; + } else { + r_stop_internal("Unexpected `missing` value, %i.", c_missing); + } +} + +// Figure out the output size +// - `size` if supplied +// - Size of 1st `conditions` element if one exists +// - Size 0 if `conditions` is empty +static +r_ssize compute_size(r_ssize size, r_obj* xs) { + if (size != -1) { + return size; + } + + if (r_length(xs) == 0) { + return 0; + } + + return r_length(r_list_get(xs, 0)); +} diff --git a/src/parallel.h b/src/parallel.h new file mode 100644 index 000000000..f57a82747 --- /dev/null +++ b/src/parallel.h @@ -0,0 +1,28 @@ +#ifndef VCTRS_PARALLEL_H +#define VCTRS_PARALLEL_H + +#include "vctrs-core.h" + +enum vec_parallel_missing { + VEC_PARALLEL_MISSING_na, + VEC_PARALLEL_MISSING_false, + VEC_PARALLEL_MISSING_true +}; + +r_obj* vec_pany( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call +); + +r_obj* vec_pall( + r_obj* xs, + enum vec_parallel_missing missing, + r_ssize size, + struct vctrs_arg* p_xs_arg, + struct r_lazy error_call +); + +#endif diff --git a/tests/testthat/_snaps/parallel.md b/tests/testthat/_snaps/parallel.md new file mode 100644 index 000000000..db4b231d7 --- /dev/null +++ b/tests/testthat/_snaps/parallel.md @@ -0,0 +1,212 @@ +# no casting is done + + Code + vec_pall(1) + Condition + Error in `vec_pall()`: + ! `..1` must be a logical vector, not the number 1. + +--- + + Code + vec_pany(1) + Condition + Error in `vec_pany()`: + ! `..1` must be a logical vector, not the number 1. + +--- + + Code + vec_pall(array(TRUE)) + Condition + Error in `vec_pall()`: + ! `..1` must be a logical vector, not a logical 1D array. + +--- + + Code + vec_pany(array(TRUE)) + Condition + Error in `vec_pany()`: + ! `..1` must be a logical vector, not a logical 1D array. + +--- + + Code + vec_pall(structure(TRUE, class = "foo")) + Condition + Error in `vec_pall()`: + ! `..1` must be a logical vector, not a object. + +--- + + Code + vec_pany(structure(TRUE, class = "foo")) + Condition + Error in `vec_pany()`: + ! `..1` must be a logical vector, not a object. + +# no recycling is done + + Code + vec_pall(TRUE, c(TRUE, TRUE, TRUE)) + Condition + Error in `vec_pall()`: + ! `..2` must have size 1, not size 3. + +--- + + Code + vec_pany(TRUE, c(TRUE, TRUE, TRUE)) + Condition + Error in `vec_pany()`: + ! `..2` must have size 1, not size 3. + +--- + + Code + vec_pall(TRUE, c(TRUE, TRUE, TRUE), .size = 3L) + Condition + Error in `vec_pall()`: + ! `..1` must have size 3, not size 1. + +--- + + Code + vec_pany(TRUE, c(TRUE, TRUE, TRUE), .size = 3L) + Condition + Error in `vec_pany()`: + ! `..1` must have size 3, not size 1. + +# validates `.missing` + + Code + vec_pall(.missing = c(TRUE, FALSE)) + Condition + Error in `vec_pall()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +--- + + Code + vec_pany(.missing = c(TRUE, FALSE)) + Condition + Error in `vec_pany()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +--- + + Code + vec_pall(.missing = 1) + Condition + Error in `vec_pall()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +--- + + Code + vec_pany(.missing = 1) + Condition + Error in `vec_pany()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +--- + + Code + vec_pall(.missing = NULL) + Condition + Error in `vec_pall()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +--- + + Code + vec_pany(.missing = NULL) + Condition + Error in `vec_pany()`: + ! `.missing` must be `NA`, `FALSE`, or `TRUE`. + +# validates `.size` + + Code + vec_pall(.size = c(1, 2)) + Condition + Error in `vec_pall()`: + ! `.size` must be a scalar integer or double. + +--- + + Code + vec_pany(.size = c(1, 2)) + Condition + Error in `vec_pany()`: + ! `.size` must be a scalar integer or double. + +--- + + Code + vec_pall(.size = 1.5) + Condition + Error in `vec_pall()`: + ! `.size` must be a whole number, not a decimal number. + +--- + + Code + vec_pany(.size = 1.5) + Condition + Error in `vec_pany()`: + ! `.size` must be a whole number, not a decimal number. + +--- + + Code + vec_pall(.size = NA_integer_) + Condition + Error in `vec_pall()`: + ! negative length vectors are not allowed + +--- + + Code + vec_pany(.size = NA_integer_) + Condition + Error in `vec_pany()`: + ! negative length vectors are not allowed + +# names are used in errors + + Code + vec_pall(1.5, .arg = "x") + Condition + Error in `vec_pall()`: + ! `x[[1]]` must be a logical vector, not the number 1.5. + +--- + + Code + vec_pall(a = 1.5, .arg = "x") + Condition + Error in `vec_pall()`: + ! `x$a` must be a logical vector, not the number 1.5. + +--- + + Code + x <- c(TRUE, FALSE) + y <- logical() + vec_pany(x, y) + Condition + Error in `vec_pany()`: + ! `..2` must have size 2, not size 0. + +--- + + Code + x <- c(TRUE, FALSE) + y <- logical() + vec_pany(a = x, b = y, .arg = "x", .error_call = quote(foo())) + Condition + Error in `foo()`: + ! `x$b` must have size 2, not size 0. + diff --git a/tests/testthat/test-parallel.R b/tests/testthat/test-parallel.R new file mode 100644 index 000000000..003d989f8 --- /dev/null +++ b/tests/testthat/test-parallel.R @@ -0,0 +1,161 @@ +test_that("9 possible variations of each combination are right", { + N <- NA + + expect_identical(vec_pall(T, T, .missing = NA), T) + expect_identical(vec_pall(T, F, .missing = NA), F) + expect_identical(vec_pall(T, N, .missing = NA), N) + expect_identical(vec_pall(F, T, .missing = NA), F) + expect_identical(vec_pall(F, F, .missing = NA), F) + expect_identical(vec_pall(F, N, .missing = NA), F) + expect_identical(vec_pall(N, T, .missing = NA), N) + expect_identical(vec_pall(N, F, .missing = NA), F) + expect_identical(vec_pall(N, N, .missing = NA), N) + + expect_identical(vec_pall(T, T, .missing = TRUE), T) + expect_identical(vec_pall(T, F, .missing = TRUE), F) + expect_identical(vec_pall(T, N, .missing = TRUE), T) + expect_identical(vec_pall(F, T, .missing = TRUE), F) + expect_identical(vec_pall(F, F, .missing = TRUE), F) + expect_identical(vec_pall(F, N, .missing = TRUE), F) + expect_identical(vec_pall(N, T, .missing = TRUE), T) + expect_identical(vec_pall(N, F, .missing = TRUE), F) + expect_identical(vec_pall(N, N, .missing = TRUE), T) + + expect_identical(vec_pall(T, T, .missing = FALSE), T) + expect_identical(vec_pall(T, F, .missing = FALSE), F) + expect_identical(vec_pall(T, N, .missing = FALSE), F) + expect_identical(vec_pall(F, T, .missing = FALSE), F) + expect_identical(vec_pall(F, F, .missing = FALSE), F) + expect_identical(vec_pall(F, N, .missing = FALSE), F) + expect_identical(vec_pall(N, T, .missing = FALSE), F) + expect_identical(vec_pall(N, F, .missing = FALSE), F) + expect_identical(vec_pall(N, N, .missing = FALSE), F) + + expect_identical(vec_pany(T, T, .missing = NA), T) + expect_identical(vec_pany(T, F, .missing = NA), T) + expect_identical(vec_pany(T, N, .missing = NA), T) + expect_identical(vec_pany(F, T, .missing = NA), T) + expect_identical(vec_pany(F, F, .missing = NA), F) + expect_identical(vec_pany(F, N, .missing = NA), N) + expect_identical(vec_pany(N, T, .missing = NA), T) + expect_identical(vec_pany(N, F, .missing = NA), N) + expect_identical(vec_pany(N, N, .missing = NA), N) + + expect_identical(vec_pany(T, T, .missing = TRUE), T) + expect_identical(vec_pany(T, F, .missing = TRUE), T) + expect_identical(vec_pany(T, N, .missing = TRUE), T) + expect_identical(vec_pany(F, T, .missing = TRUE), T) + expect_identical(vec_pany(F, F, .missing = TRUE), F) + expect_identical(vec_pany(F, N, .missing = TRUE), T) + expect_identical(vec_pany(N, T, .missing = TRUE), T) + expect_identical(vec_pany(N, F, .missing = TRUE), T) + expect_identical(vec_pany(N, N, .missing = TRUE), T) + + expect_identical(vec_pany(T, T, .missing = FALSE), T) + expect_identical(vec_pany(T, F, .missing = FALSE), T) + expect_identical(vec_pany(T, N, .missing = FALSE), T) + expect_identical(vec_pany(F, T, .missing = FALSE), T) + expect_identical(vec_pany(F, F, .missing = FALSE), F) + expect_identical(vec_pany(F, N, .missing = FALSE), F) + expect_identical(vec_pany(N, T, .missing = FALSE), T) + expect_identical(vec_pany(N, F, .missing = FALSE), F) + expect_identical(vec_pany(N, N, .missing = FALSE), F) +}) + +test_that("works with empty inputs", { + expect_identical(vec_pall(logical(), logical()), logical()) + expect_identical(vec_pany(logical(), logical()), logical()) +}) + +test_that("works with no inputs", { + expect_identical(vec_pall(), logical()) + expect_identical(vec_pany(), logical()) +}) + +test_that("works with no inputs and specified `.size`", { + expect_identical(vec_pall(.size = 3), c(TRUE, TRUE, TRUE)) + expect_identical(vec_pany(.size = 3), c(FALSE, FALSE, FALSE)) +}) + +test_that("no casting is done", { + expect_snapshot(error = TRUE, { + vec_pall(1) + }) + expect_snapshot(error = TRUE, { + vec_pany(1) + }) + + # Arrays + expect_snapshot(error = TRUE, { + vec_pall(array(TRUE)) + }) + expect_snapshot(error = TRUE, { + vec_pany(array(TRUE)) + }) + + # Class + expect_snapshot(error = TRUE, { + vec_pall(structure(TRUE, class = "foo")) + }) + expect_snapshot(error = TRUE, { + vec_pany(structure(TRUE, class = "foo")) + }) +}) + +test_that("no recycling is done", { + expect_snapshot(error = TRUE, { + vec_pall(TRUE, c(TRUE, TRUE, TRUE)) + }) + expect_snapshot(error = TRUE, { + vec_pany(TRUE, c(TRUE, TRUE, TRUE)) + }) + + # With `.size` + expect_snapshot(error = TRUE, { + vec_pall(TRUE, c(TRUE, TRUE, TRUE), .size = 3L) + }) + expect_snapshot(error = TRUE, { + vec_pany(TRUE, c(TRUE, TRUE, TRUE), .size = 3L) + }) +}) + +test_that("validates `.missing`", { + expect_snapshot(error = TRUE, vec_pall(.missing = c(TRUE, FALSE))) + expect_snapshot(error = TRUE, vec_pany(.missing = c(TRUE, FALSE))) + + expect_snapshot(error = TRUE, vec_pall(.missing = 1)) + expect_snapshot(error = TRUE, vec_pany(.missing = 1)) + + expect_snapshot(error = TRUE, vec_pall(.missing = NULL)) + expect_snapshot(error = TRUE, vec_pany(.missing = NULL)) +}) + +test_that("validates `.size`", { + expect_snapshot(error = TRUE, vec_pall(.size = c(1, 2))) + expect_snapshot(error = TRUE, vec_pany(.size = c(1, 2))) + + expect_snapshot(error = TRUE, vec_pall(.size = 1.5)) + expect_snapshot(error = TRUE, vec_pany(.size = 1.5)) + + expect_snapshot(error = TRUE, vec_pall(.size = NA_integer_)) + expect_snapshot(error = TRUE, vec_pany(.size = NA_integer_)) +}) + +test_that("names are used in errors", { + expect_snapshot(error = TRUE, { + vec_pall(1.5, .arg = "x") + }) + expect_snapshot(error = TRUE, { + vec_pall(a = 1.5, .arg = "x") + }) + expect_snapshot(error = TRUE, { + x <- c(TRUE, FALSE) + y <- logical() + vec_pany(x, y) + }) + expect_snapshot(error = TRUE, { + x <- c(TRUE, FALSE) + y <- logical() + vec_pany(a = x, b = y, .arg = "x", .error_call = quote(foo())) + }) +})