tests/testthat/test-dots-info.R

# Basic operations --------------------------------------------------------

test_that("env_dots_exist() detects dots presence", {
  fn <- function(...) env_dots_exist()
  fn_no_dots <- function() env_dots_exist()

  expect_true(fn())
  expect_true(fn(a = 1))
  expect_false(fn_no_dots())
})

test_that("env_dots_length() returns correct count", {
  fn <- function(...) env_dots_length()

  expect_equal(fn(), 0L)
  expect_equal(fn(1), 1L)
  expect_equal(fn(a = 1, b = 2), 2L)
  expect_equal(fn(1, 2, 3, 4, 5), 5L)
  expect_equal(fn(1, , 3), 3L)
})

test_that("env_dots_length() errors without dots", {
  fn <- function() env_dots_length()
  expect_error(fn(), "incorrect context")
})

test_that("env_dots_names() returns names", {
  fn <- function(...) env_dots_names()

  expect_null(fn())
  expect_null(fn(1, 2))
  expect_equal(fn(a = 1, b = 2), c("a", "b"))
  expect_equal(fn(a = 1, 2, c = 3), c("a", "", "c"))
})

test_that("env_dots_names() errors without dots", {
  fn <- function() env_dots_names()
  expect_error(fn(), "incorrect context")
})

test_that("env_dot_get() evaluates and returns dot value", {
  fn <- function(...) env_dot_get(environment(), 1)

  expect_equal(fn(1 + 1), 2)
  expect_equal(fn(10), 10)

  x <- 100
  expect_equal(fn(x), 100)
})

test_that("env_dot_get() can access different positions", {
  fn <- function(...) {
    env <- environment()
    list(env_dot_get(env, 1), env_dot_get(env, 2), env_dot_get(env, 3))
  }
  expect_equal(fn("a", "b", "c"), list("a", "b", "c"))
})

test_that("env_dot_get() returns missing arg for missing dot", {
  fn <- function(...) env_dot_get(environment(), 1)
  expect_true(is_missing(fn(, 2)))
})

test_that("env_dot_get() respects index bounds", {
  fn <- function(...) env_dot_get(environment(), 2)
  fn_no_dots <- function() env_dot_get(environment(), 1)
  fn_empty <- function(...) env_dot_get(environment(), 1)

  expect_error(fn(1), "fewer than")
  expect_error(fn_no_dots(), "incorrect context")
  expect_error(fn_empty(), "fewer than")
})


# Type classification -----------------------------------------------------

test_that("env_dot_type() identifies delayed promises", {
  fn <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }

  expect_equal(fn(x), "delayed")
  expect_equal(fn(1 + 1), "delayed")
  expect_equal(fn(42), "delayed")
})

test_that("env_dot_type() detects missing arguments", {
  fn <- function(...) {
    env <- environment()
    n <- env_dots_length()
    vapply(seq_len(n), function(i) env_dot_type(env, i), character(1))
  }

  expect_equal(fn(a, ), c("delayed", "missing"))
  expect_equal(fn(, b, ), c("missing", "delayed", "missing"))
})

test_that("env_dot_type() detects forced promises", {
  fn <- function(...) {
    env <- environment()
    type_before <- env_dot_type(env, 1)
    env_dot_get(env, 1)
    type_after <- env_dot_type(env, 1)
    c(before = type_before, after = type_after)
  }

  result <- fn(1 + 1)
  expect_equal(result[["before"]], "delayed")
  expect_equal(result[["after"]], "forced")
})

test_that("env_dot_type() detects value dots from compiler", {
  fn <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  wrapper <- compiler::cmpfun(function() fn("hello"))
  expect_equal(wrapper(), "value")
})

test_that("env_dot_type() classifies mixed types correctly", {
  fn <- function(...) {
    env <- environment()
    env_dot_get(env, 2)
    n <- env_dots_length()
    vapply(seq_len(n), function(i) env_dot_type(env, i), character(1))
  }

  # Dot 1 is delayed, dot 2 is forced (we evaluated it), dot 3 is missing
  expect_equal(fn(a, 1 + 1, ), c("delayed", "forced", "missing"))
})


# Delayed accessors -------------------------------------------------------

test_that("env_dot_delayed_expr() returns promise expression", {
  fn <- function(...) {
    env <- environment()
    env_dot_delayed_expr(env, 1)
  }

  expect_equal(fn(x + y), quote(x + y))
  expect_equal(fn(foo), quote(foo))
  expect_equal(fn(42), 42)
})

test_that("env_dot_delayed_env() returns promise environment", {
  fn <- function(...) {
    env <- environment()
    env_dot_delayed_env(env, 1)
  }

  e <- new.env()
  result <- with(e, fn(x + 1))
  expect_identical(result, e)
})

test_that("env_dot_delayed_expr() errors on forced promise", {
  fn <- function(...) {
    env <- environment()
    env_dot_get(env, 1)
    env_dot_delayed_expr(env, 1)
  }
  expect_error(fn(1 + 1), "not a delayed \\.\\.\\.")
})

test_that("env_dot_delayed_env() errors on forced promise", {
  fn <- function(...) {
    env <- environment()
    env_dot_get(env, 1)
    env_dot_delayed_env(env, 1)
  }
  expect_error(fn(1 + 1), "not a delayed \\.\\.\\.")
})

test_that("env_dot_delayed_expr() errors on missing argument", {
  fn <- function(...) {
    env <- environment()
    env_dot_delayed_expr(env, 2)
  }
  expect_error(fn(a, ), "not a delayed \\.\\.\\.")
})

test_that("env_dot_delayed_env() errors on missing argument", {
  fn <- function(...) {
    env <- environment()
    env_dot_delayed_env(env, 2)
  }
  expect_error(fn(a, ), "not a delayed \\.\\.\\.")
})


# Forced accessor ---------------------------------------------------------

test_that("env_dot_forced_expr() returns expression from forced promise", {
  fn <- function(...) {
    env <- environment()
    env_dot_get(env, 1)
    env_dot_forced_expr(env, 1)
  }

  # The exact value of env_dot_forced_expr depends on R internals (JIT state),
  # so just verify it succeeds without error
  expect_no_error(fn(1 + 1))
  expect_no_error(fn(42))
})

test_that("env_dot_forced_expr() errors on delayed promise", {
  fn <- function(...) {
    env <- environment()
    env_dot_forced_expr(env, 1)
  }
  expect_error(fn(1 + 1), "not a forced \\.\\.\\.")
})

test_that("env_dot_forced_expr() errors on missing argument", {
  fn <- function(...) {
    env <- environment()
    env_dot_get(env, 1)
    env_dot_forced_expr(env, 2)
  }
  expect_error(fn(1, ), "not a forced \\.\\.\\.")
})


# Forwarding with `...` (shared promise) ----------------------------------

test_that("`...` forwarding shares the promise object", {
  inner <- function(...) {
    env <- environment()
    list(
      expr = env_dot_delayed_expr(env, 1),
      env = env_dot_delayed_env(env, 1)
    )
  }
  outer <- function(...) inner(...)

  caller_env <- current_env()
  result <- outer(x + y)

  # Shared promise preserves the original expression and environment
  expect_equal(result$expr, quote(x + y))
  expect_identical(result$env, caller_env)
})

test_that("`...` forwarding preserves types", {
  inner <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  outer <- function(...) inner(...)

  expect_equal(outer(x), "delayed")
})

test_that("`...` forwarding reflects forced state", {
  inner <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  outer <- function(...) {
    force(..1)
    inner(...)
  }

  expect_equal(outer(1 + 1), "forced")
})

test_that("env_dot_get() works through `...` forwarding", {
  inner <- function(...) env_dot_get(environment(), 1)
  outer <- function(...) inner(...)

  x <- 42
  expect_equal(outer(x), 42)
  expect_equal(outer(1 + 1), 2)
})

test_that("env_dots_length() reflects forwarded count", {
  inner <- function(...) env_dots_length()
  outer <- function(...) inner(...)

  expect_equal(outer(a, b, c), 3L)
})

test_that("env_dots_names() preserved through `...` forwarding", {
  inner <- function(...) env_dots_names()
  outer <- function(...) inner(...)

  expect_equal(outer(a = 1, b = 2), c("a", "b"))
  expect_equal(outer(1, b = 2, 3), c("", "b", ""))
  expect_null(outer(1, 2, 3))
})

test_that("`...` forwarding across multiple levels", {
  level3 <- function(...) {
    env <- environment()
    list(
      type = env_dot_type(env, 1),
      expr = env_dot_delayed_expr(env, 1),
      env = env_dot_delayed_env(env, 1)
    )
  }
  level2 <- function(...) level3(...)
  level1 <- function(...) level2(...)

  caller_env <- current_env()
  result <- level1(x + y)

  expect_equal(result$type, "delayed")
  expect_equal(result$expr, quote(x + y))
  expect_identical(result$env, caller_env)
})


# Forwarding with `..N` (new promise) ------------------------------------

test_that("`..N` forwarding creates a new promise layer", {
  inner <- function(...) {
    env <- environment()
    list(
      expr = env_dot_delayed_expr(env, 1),
      env = env_dot_delayed_env(env, 1)
    )
  }
  outer <- function(...) inner(..1)

  result <- outer(x + y)

  # New promise wrapping `..1`, so expr is `..1` and env is outer's frame
  expect_equal(result$expr, quote(..1))
  expect_false(identical(result$env, current_env()))
})

test_that("`..N` forwarding can select specific dots", {
  inner <- function(...) {
    env <- environment()
    list(
      n = env_dots_length(),
      expr1 = env_dot_delayed_expr(env, 1),
      expr2 = env_dot_delayed_expr(env, 2)
    )
  }
  outer <- function(...) inner(..2, ..1)

  result <- outer(a, b)
  expect_equal(result$n, 2L)
  expect_equal(result$expr1, quote(..2))
  expect_equal(result$expr2, quote(..1))
})

test_that("env_dot_get() works through `..N` forwarding", {
  inner <- function(...) env_dot_get(environment(), 1)
  outer <- function(...) inner(..1)

  x <- 42
  expect_equal(outer(x), 42)
  expect_equal(outer(1 + 1), 2)
})

test_that("`..N` forwarding with forced outer dot shows delayed for new promise", {
  inner <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  outer <- function(...) {
    force(..1)
    inner(..1)
  }

  # The new promise wrapping `..1` is itself delayed,
  # even though the underlying `..1` in outer is forced
  expect_equal(outer(1 + 1), "delayed")
})

test_that("`..N` forwarding across multiple levels", {
  level3 <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  level2 <- function(...) level3(..1)
  level1 <- function(...) level2(..1)

  expect_equal(level1(x), "delayed")
})


# Compiler value dots -----------------------------------------------------

test_that("compiler-unwrapped literals create value dots", {
  fn <- function(...) {
    env <- environment()
    env_dot_type(env, 1)
  }
  wrapper <- compiler::cmpfun(function() fn("hello"))
  expect_equal(wrapper(), "value")

  wrapper2 <- compiler::cmpfun(function() fn(42L))
  expect_equal(wrapper2(), "value")
})

test_that("env_dot_get() works with value dots", {
  fn <- function(...) env_dot_get(environment(), 1)
  wrapper <- compiler::cmpfun(function() fn("hello"))
  expect_equal(wrapper(), "hello")
})

test_that("delayed accessors error on value dots", {
  fn_expr <- function(...) {
    env <- environment()
    env_dot_delayed_expr(env, 1)
  }
  fn_env <- function(...) {
    env <- environment()
    env_dot_delayed_env(env, 1)
  }

  wrapper_expr <- compiler::cmpfun(function() fn_expr("hello"))
  wrapper_env <- compiler::cmpfun(function() fn_env("hello"))

  expect_error(wrapper_expr(), "not a delayed \\.\\.\\.")
  expect_error(wrapper_env(), "not a delayed \\.\\.\\.")
})


# Input validation --------------------------------------------------------

test_that("index validation works", {
  expect_error(env_dot_type(environment(), 0), "larger than or equal to 1")
  expect_error(env_dot_type(environment(), -1), "larger than or equal to 1")
  expect_error(env_dot_type(environment(), "a"), "number")
})

test_that("environment validation works", {
  expect_error(env_dots_exist(NULL), "environment")
  expect_error(env_dots_length(1), "environment")
  expect_error(env_dots_names(list()), "environment")
})


# Frame-only lookup -------------------------------------------------------

test_that("env_dots_exist() does not reach into parent envs", {
  fn <- function(...) local(env_dots_exist())
  fn_no_dots <- function() local(env_dots_exist())

  expect_false(fn())
  expect_false(fn(1))
  expect_false(fn_no_dots())
})

test_that("env_dots_exist() only returns TRUE for DOTSXP values", {
  e <- new.env(parent = emptyenv())

  e$... <- 1
  expect_false(env_dots_exist(e))

  e$... <- list(1, 2)
  expect_false(env_dots_exist(e))

  e$... <- NULL
  expect_false(env_dots_exist(e))
})

test_that("env_dots_length() does not reach into parent envs", {
  fn <- function(...) local(env_dots_length())
  expect_error(fn(1, 2, 3), "incorrect context")
})

test_that("env_dots_names() does not reach into parent envs", {
  fn <- function(...) local(env_dots_names())
  expect_error(fn(a = 1, b = 2), "incorrect context")
})

test_that("env_dot_get() does not reach into parent envs", {
  fn <- function(...) local(env_dot_get(environment(), 1))
  expect_error(fn(42), "incorrect context")
})

test_that("env_dot_type() does not reach into parent envs", {
  fn <- function(...) local(env_dot_type(environment(), 1))
  expect_error(fn(x), "incorrect context")
})

test_that("env_dot_delayed_expr() does not reach into parent envs", {
  fn <- function(...) local(env_dot_delayed_expr(environment(), 1))
  expect_error(fn(x + y), "incorrect context")
})

test_that("env_dot_delayed_env() does not reach into parent envs", {
  fn <- function(...) local(env_dot_delayed_env(environment(), 1))
  expect_error(fn(x + 1), "incorrect context")
})


# Promise chain unwrapping ------------------------------------------------
# `...` expansion via `promiseArgs()` wraps each dot element with
# `mkPROMISE(CAR(h), rho)`, creating a promise chain where PRCODE of
# the outer promise is the original inner PROMSXP.

test_that("env_dot_type() unwraps chains to detect forced state", {
  inner <- function(...) env_dot_type(environment(), 1)
  outer <- function(...) { force(..1); inner(...) }
  expect_equal(outer(1 + 1), "forced")
})

test_that("env_dot_type() unwraps chains to detect delayed state", {
  inner <- function(...) env_dot_type(environment(), 1)
  outer <- function(...) inner(...)
  expect_equal(outer(1 + 1), "delayed")
})

test_that("env_dot_delayed_expr() unwraps chains to the outermost expression", {
  inner <- function(...) env_dot_delayed_expr(environment(), 1)
  outer <- function(...) inner(...)

  caller_env <- current_env()
  result <- outer(x + y)

  expect_equal(result, quote(x + y))
})

test_that("env_dot_delayed_env() unwraps chains to the outermost environment", {
  inner <- function(...) env_dot_delayed_env(environment(), 1)
  outer <- function(...) inner(...)

  caller_env <- current_env()
  result <- outer(x + y)

  expect_identical(result, caller_env)
})

test_that("env_dot_forced_expr() unwraps chains to detect forced promise", {
  inner <- function(...) {
    env <- environment()
    env_dot_get(env, 1)
    env_dot_forced_expr(env, 1)
  }
  outer <- function(...) inner(...)
  expect_no_error(outer(1 + 1))
})

test_that("env_dot_delayed_expr() errors on forced chain", {
  inner <- function(...) env_dot_delayed_expr(environment(), 1)
  outer <- function(...) { force(..1); inner(...) }
  expect_error(outer(1 + 1), "not a delayed \\.\\.\\.")
})

test_that("env_dot_delayed_env() errors on forced chain", {
  inner <- function(...) env_dot_delayed_env(environment(), 1)
  outer <- function(...) { force(..1); inner(...) }
  expect_error(outer(1 + 1), "not a delayed \\.\\.\\.")
})

test_that("deeper `...` chains unwrap correctly", {
  inner <- function(...) env_dot_type(environment(), 1)
  mid <- function(...) inner(...)
  outer <- function(...) { force(..1); mid(...) }
  expect_equal(outer(1 + 1), "forced")
})

Try the rlang package in your browser

Any scripts or data that you put into this service are public.

rlang documentation built on April 6, 2026, 5:07 p.m.