R/30-api-flexseq-indexing.R

Defines functions `[[<-.flexseq` `[<-.flexseq` `[[.flexseq` .ord_normalize_selector_positions `[.flexseq` .ft_update_name_map .ft_effective_name .ft_name_from_value .ft_get_elems_at .ft_get_elem_at .ft_match_name_indices .ft_find_name_position .ft_find_name_position_impl .ft_child_size .ft_name_positions_fast .ft_name_positions .ft_assert_name_state .ft_collect_names .ft_prepare_subset_entries .ft_strip_name .ft_values_have_name_hints .ft_recycle_values .ft_name_map_threshold .ft_true_positions .ft_assert_lgl_indices .ft_assert_chr_indices .ft_assert_int_indices .ft_to_list .ft_to_list_fill

#SO

# recursive fill helper for `.ft_to_list` to avoid repeated list concatenation.
# Runtime: O(n) in number of elements visited.
.ft_to_list_fill <- function(x, st) {
  if(!is_structural_node(x)) {
    st$out[[st$pos]] <- x
    st$pos <- st$pos + 1L
    return(invisible(NULL))
  }
  if(x %isa% Empty) {
    return(invisible(NULL))
  }
  if(x %isa% Single) {
    .ft_to_list_fill(.subset2(x, 1), st)
    return(invisible(NULL))
  }
  if(x %isa% Deep) {
    .ft_to_list_fill(.subset2(x, "prefix"), st)
    .ft_to_list_fill(.subset2(x, "middle"), st)
    .ft_to_list_fill(.subset2(x, "suffix"), st)
    return(invisible(NULL))
  }
  for(el in x) {
    .ft_to_list_fill(el, st)
  }
  invisible(NULL)
}

# flatten a fingertree into its element sequence (left-to-right)
# Runtime: O(n) in number of elements.
.ft_to_list <- function(x) {
  if(!is_structural_node(x)) {
    return(list(x))
  }
  n <- as.integer(node_measure(x, ".size"))
  if(n == 0L) {
    return(list())
  }
  st <- new.env(parent = emptyenv())
  st$out <- vector("list", n)
  st$pos <- 1L
  .ft_to_list_fill(x, st)
  st$out
}

# Runtime: O(k), where k = length(idx).
.ft_assert_int_indices <- function(idx, n) {
  if(is.null(idx)) {
    stop("Index is required.")
  }
  if(!is.numeric(idx) || any(is.na(idx)) || any(idx != as.integer(idx))) {
    stop("Only non-missing integer indices are supported.")
  }
  idx <- as.integer(idx)
  if(length(idx) == 0L) {
    return(idx)
  }
  if(any(idx <= 0L)) {
    stop("Only positive integer indices are supported.")
  }
  if(any(idx > n)) {
    stop("Index out of bounds.")
  }
  idx
}

# Runtime: O(k), where k = length(idx).
.ft_assert_chr_indices <- function(idx) {
  if(is.null(idx)) {
    stop("Index is required.")
  }
  if(!is.character(idx) || any(is.na(idx))) {
    stop("Only non-missing character indices are supported.")
  }
  idx
}

# Runtime: O(n) due `rep_len(..., n)`.
.ft_assert_lgl_indices <- function(idx, n) {
  if(is.null(idx)) {
    stop("Index is required.")
  }
  if(!is.logical(idx)) {
    stop("Only logical indices are supported.")
  }
  if(any(is.na(idx))) {
    stop("Logical indices cannot contain NA.")
  }
  if(length(idx) == 0L) {
    return(logical(0))
  }
  rep_len(idx, n)
}

# Runtime: O(n) worst-case in relevant input/subtree size.
.ft_true_positions <- function(mask) {
  as.integer(which(mask))
}

# minimum character-query width at which full name->position map lookup is used.
# Runtime: O(1).
.ft_name_map_threshold <- function() {
  # Tuned internal constant: switch to one-pass name->position map lookup for
  # medium/large character queries; keep scalar find loop for short vectors.
  8L
}

# Runtime: O(target_len) in recycled output size.
.ft_recycle_values <- function(values, target_len) {
  if(target_len == 0L) {
    return(list())
  }
  if(length(values) == 0L) {
    stop("Replacement has length 0 but selected positions are non-empty.")
  }
  if(length(values) == target_len) {
    return(values)
  }
  rep(values, length.out = target_len)
}

# check whether replacement payloads provide any explicit name hints
# (outer list names, ft_name attrs, or inline scalar names()).
# Runtime: O(k), where k = number of replacement values.
.ft_values_have_name_hints <- function(values) {
  vn <- names(values)
  if(!is.null(vn) && any(!is.na(vn) & vn != "")) {
    return(TRUE)
  }
  if(length(values) == 0L) {
    return(FALSE)
  }
  for(v in values) {
    if(!is.null(.ft_get_name(v)) || !is.null(.ft_name_from_value(v))) {
      return(TRUE)
    }
  }
  FALSE
}

# remove internal naming metadata from user-visible element returns.
# Runtime: O(1).
.ft_strip_name <- function(el) {
  attr(el, "ft_name") <- NULL
  el
}

# prepare selected entries for subset rebuild while preserving name invariants:
# keep names only when all selected entries are named uniquely; otherwise drop all.
# Runtime: O(k), where k = number of selected entries.
.ft_prepare_subset_entries <- function(out) {
  if(length(out) == 0L) {
    return(out)
  }
  nms <- vapply(out, function(el) {
    nm <- .ft_get_name(el)
    if(is.null(nm)) NA_character_ else nm
  }, character(1))
  if(any(is.na(nms)) || any(nms == "") || anyDuplicated(nms) > 0L) {
    return(lapply(out, .ft_strip_name))
  }
  out
}

# collect internal element names in left-to-right order.
# Runtime: O(n).
.ft_collect_names <- function(t) {
  els <- .ft_to_list(t)
  if(length(els) == 0L) {
    return(character(0))
  }
  vapply(els, function(el) {
    nm <- .ft_get_name(el)
    if(is.null(nm)) NA_character_ else nm
  }, character(1))
}

# validate global naming invariant:
# - unnamed tree: .named_count == 0
# - fully named tree: .named_count == .size and names are unique/non-empty
# Runtime: O(n) in the fully-named case (collect/scan all names).
# Intended use: correctness auditing in tests/debugging and explicit validators.
.ft_assert_name_state <- function(t) {
  if(!is_structural_node(t)) {
    return(invisible(TRUE))
  }
  n <- as.integer(node_measure(t, ".size"))
  nn <- as.integer(node_measure(t, ".named_count"))
  if(n == 0L) {
    if(nn != 0L) {
      stop("Invalid name state: empty tree has non-zero named element count.")
    }
    return(invisible(TRUE))
  }
  if(nn == 0L) {
    return(invisible(TRUE))
  }
  if(nn != n) {
    stop("Invalid name state: mixed named and unnamed elements are not allowed.")
  }
  nms <- .ft_collect_names(t)
  if(any(is.na(nms) | nms == "")) {
    stop("Invalid name state: all element names must be non-empty.")
  }
  if(anyDuplicated(nms) > 0L) {
    stop("Invalid name state: element names must be unique.")
  }
  invisible(TRUE)
}

# build a deterministic name -> position map for fully named trees.
# Runtime: O(n).
.ft_name_positions <- function(t) {
  n <- as.integer(node_measure(t, ".size"))
  nn <- as.integer(node_measure(t, ".named_count"))
  if(n == 0L || nn == 0L) {
    stop("Tree has no element names.")
  }
  if(nn != n) {
    stop("Invalid name state: mixed named and unnamed elements are not allowed.")
  }
  nms <- .ft_collect_names(t)
  pos <- seq_len(length(nms))
  names(pos) <- nms
  pos
}

# build name->position map, using C++ when available.
# Runtime: O(n).
.ft_name_positions_fast <- function(t) {
  if(.ft_cpp_enabled()) {
    return(.ft_cpp_name_positions(t))
  }
  .ft_name_positions(t)
}

# measure contribution of one child in terms of leaf-element count.
# Runtime: O(1) using cached `.size` for structural children.
.ft_child_size <- function(el) {
  if(is_structural_node(el)) {
    return(as.integer(node_measure(el, ".size")))
  }
  1L
}

# recursive linear search for one name with early exit.
# Runtime: O(n) worst-case, O(k) until first match.
.ft_find_name_position_impl <- function(x, target, offset = 0L) {
  if(!is_structural_node(x)) {
    nm <- .ft_get_name(x)
    if(!is.null(nm) && identical(nm, target)) {
      return(offset + 1L)
    }
    return(NA_integer_)
  }
  if(x %isa% Empty) {
    return(NA_integer_)
  }
  if(x %isa% Single) {
    return(.ft_find_name_position_impl(.subset2(x, 1), target, offset))
  }
  if(x %isa% Deep) {
    p <- .ft_find_name_position_impl(.subset2(x, "prefix"), target, offset)
    if(!is.na(p)) {
      return(p)
    }
    offset <- offset + as.integer(node_measure(.subset2(x, "prefix"), ".size"))
    p <- .ft_find_name_position_impl(.subset2(x, "middle"), target, offset)
    if(!is.na(p)) {
      return(p)
    }
    offset <- offset + as.integer(node_measure(.subset2(x, "middle"), ".size"))
    return(.ft_find_name_position_impl(.subset2(x, "suffix"), target, offset))
  }
  for(el in x) {
    p <- .ft_find_name_position_impl(el, target, offset)
    if(!is.na(p)) {
      return(p)
    }
    offset <- offset + .ft_child_size(el)
  }
  NA_integer_
}

# find one name position without constructing full name->position map.
# Runtime: O(n) worst-case.
.ft_find_name_position <- function(t, target) {
  .ft_find_name_position_impl(t, target, 0L)
}

# match requested names to positions. If strict_missing is FALSE, missing names
# are represented as NA integer placeholders.
# Runtime: adaptive by query width:
# - scalar query: O(n_lookup) (typically O(n) worst-case linear name scan),
# - short vector queries: O(k * n_lookup),
# - wide queries: O(n + k) via one-pass name->position map.
# where n = tree size and k = length(idx).
.ft_match_name_indices <- function(t, idx, strict_missing = FALSE) {
  n <- as.integer(node_measure(t, ".size"))
  nn <- as.integer(node_measure(t, ".named_count"))
  if(n == 0L || nn == 0L) {
    stop("Tree has no element names.")
  }
  if(nn != n) {
    stop("Invalid name state: mixed named and unnamed elements are not allowed.")
  }
  if(length(idx) == 1L) {
    p <- if(.ft_cpp_enabled()) .ft_cpp_find_name_position(t, idx[[1]]) else .ft_find_name_position(t, idx[[1]])
    if(isTRUE(strict_missing) && is.na(p)) {
      stop("Unknown element name(s): ", idx[[1]])
    }
    return(as.integer(p))
  }

  if(length(idx) >= .ft_name_map_threshold()) {
    name_to_pos <- .ft_name_positions_fast(t)
    pos <- unname(name_to_pos[idx])
    if(isTRUE(strict_missing) && any(is.na(pos))) {
      missing_names <- unique(idx[is.na(pos)])
      stop("Unknown element name(s): ", paste(missing_names, collapse = ", "))
    }
    return(as.integer(pos))
  }

  pos <- integer(length(idx))
  for(k in seq_along(idx)) {
    p <- if(.ft_cpp_enabled()) .ft_cpp_find_name_position(t, idx[[k]]) else .ft_find_name_position(t, idx[[k]])
    if(isTRUE(strict_missing) && is.na(p)) {
      stop("Unknown element name(s): ", idx[[k]])
    }
    pos[[k]] <- p
  }
  as.integer(pos)
}

# internal positional read that preserves ft_name metadata.
# Runtime: O(log n) via indexed locate path (C++ or R fallback).
.ft_get_elem_at <- function(x, idx) {
  if(.ft_cpp_enabled()) {
    return(.ft_cpp_get_by_index(x, idx))
  }
  ctx <- resolve_named_monoid(x, ".size")
  hit <- locate_tree_impl_fast(
    function(v) v >= idx,
    ctx$monoid$i,
    x,
    ctx$monoids,
    ctx$monoid,
    ".size",
    0L
  )
  if(!isTRUE(hit$found)) {
    stop("Index out of bounds.")
  }
  hit$value
}

# bulk positional read helper preserving ft_name metadata.
# Runtime: O(k log n) fallback, where k = length(idx); C++ path is typically faster.
.ft_get_elems_at <- function(x, idx) {
  if(length(idx) == 0L) {
    return(list())
  }
  if(.ft_cpp_enabled()) {
    return(.ft_cpp_get_many_by_index(x, idx))
  }
  lapply(idx, function(i) .ft_get_elem_at(x, i))
}

# extract a name carried directly by a scalar replacement value.
# Runtime: O(1).
.ft_name_from_value <- function(el) {
  nms <- names(el)
  if(is.null(nms) || length(nms) != 1L) {
    return(NULL)
  }
  .ft_normalize_name(nms[[1]])
}

# effective replacement name: explicit outer list name first, then existing
# internal name, then name carried by the replacement value itself.
# Runtime: O(1).
.ft_effective_name <- function(el, explicit_name = NULL) {
  nm <- .ft_normalize_name(explicit_name)
  if(is.null(nm)) {
    nm <- .ft_get_name(el)
  }
  if(is.null(nm)) {
    nm <- .ft_name_from_value(el)
  }
  nm
}

# update name mapping for sequential replacement and enforce uniqueness.
# Runtime: O(k), where k is current map size.
.ft_update_name_map <- function(name_to_pos, name_vec, pos, nm) {
  old_nm <- name_vec[[pos]]
  if(identical(nm, old_nm)) {
    return(list(name_to_pos = name_to_pos, name_vec = name_vec))
  }
  existing <- if(nm %in% names(name_to_pos)) as.integer(name_to_pos[[nm]]) else NULL
  if(!is.null(existing) && existing != pos) {
    stop("Element names must be unique.")
  }
  name_to_pos <- name_to_pos[names(name_to_pos) != old_nm]
  name_to_pos[[nm]] <- pos
  name_vec[[pos]] <- nm
  list(name_to_pos = name_to_pos, name_vec = name_vec)
}

#' Flexseq Indexing
#'
#' Index, replace, and extract elements of a `flexseq` by position or name.
#'
#' @method [ flexseq
#' @param x A `flexseq`.
#' @param i Positive integer indices, character element names, or logical mask.
#'   For `[[`, a single integer or character name.
#' @param ... Unused.
#' @return For `[`: a new `flexseq` containing selected elements in query order.
#'   For character indexing, missing names are represented as `NULL` elements.
#' @details
#' Selector behavior:
#' - Integer indexing (`[`) returns elements in the requested order.
#' - Character indexing (`[`) returns elements in requested name order; unknown
#'   names become `NULL` elements in the output.
#' - Logical indexing (`[`) is positional and follows logical-mask selection.
#'
#' Replacement behavior:
#' - `[<-` is persistent and recycles `value` to the number of selected
#'   positions (with standard R recycling warnings where applicable).
#' - `[[<-` replaces one element; assigning `NULL` removes that element.
#' @examples
#' x <- as_flexseq(letters[1:6])
#' x
#'
#' x2 <- x[c(2, 4, 6)]
#' x2
#'
#' # named lookups return NULL for missing names
#' x3 <- as_flexseq(setNames(as.list(letters[1:4]), c("w", "x", "y", "z")))
#' x4 <- x3[c("y", "missing", "w")]
#' x4
#'
#' # logical indexing supports recycling
#' x[c(TRUE, FALSE)]
#' @export
# Runtime: integer/logical reads O(k log n) in fallback paths (C++ paths are
# typically faster), where k = selected positions. Character reads are adaptive:
# O(k * n_lookup) for short queries, O(n + k) for wider queries via name map.
# In all branches, rebuilding the output tree from selected elements is linear
# in output size.
`[.flexseq` <- function(x, i, ...) {
  if(missing(i)) {
    return(x)
  }
  ms <- resolve_tree_monoids(x, required = TRUE)
  n <- as.integer(node_measure(x, ".size"))

  if(is.logical(i)) {
    mask <- .ft_assert_lgl_indices(i, n)
    idx <- .ft_true_positions(mask)
    if(length(idx) == 0L) {
      return(.ft_restore_subclass(empty_tree(monoids = ms), x, context = "["))
    }
    out <- .ft_prepare_subset_entries(.ft_get_elems_at(x, idx))
    return(.ft_restore_subclass(tree_from(out, monoids = ms), x, context = "["))
  }

  if(is.character(i)) {
    idx <- .ft_assert_chr_indices(i)
    if(length(idx) == 0L) {
      return(.ft_restore_subclass(empty_tree(monoids = ms), x, context = "["))
    }

    strict_missing <- inherits(x, "priority_queue")
    pos <- .ft_match_name_indices(x, idx, strict_missing = strict_missing)
    out <- vector("list", length(pos))
    valid <- which(!is.na(pos))
    if(length(valid) > 0L) {
      vals <- .ft_get_elems_at(x, as.integer(pos[valid]))
      for(j in seq_along(valid)) {
        out[[valid[[j]]]] <- vals[[j]]
      }
      out <- .ft_prepare_subset_entries(out)
    } else {
      out <- .ft_prepare_subset_entries(out)
    }
    return(.ft_restore_subclass(tree_from(out, monoids = ms), x, context = "["))
  }

  idx <- .ft_assert_int_indices(i, n)
  if(length(idx) == 0L) {
    return(.ft_restore_subclass(empty_tree(monoids = ms), x, context = "["))
  }
  out <- .ft_prepare_subset_entries(.ft_get_elems_at(x, idx))
  .ft_restore_subclass(tree_from(out, monoids = ms), x, context = "[")
}

# Runtime: O(k), where k = length(pos).
.ord_normalize_selector_positions <- function(pos) {
  pos <- as.integer(pos)
  if(length(pos) == 0L) {
    return(pos)
  }
  if(any(is.na(pos))) {
    stop("Ordered subsetting does not allow missing indices.")
  }
  if(anyDuplicated(pos) > 0L) {
    stop("Ordered subsetting does not allow duplicate indices.")
  }
  sorted <- sort(pos)
  if(length(sorted) > 1L && !identical(pos, sorted)) {
    warning(
      "Ordered subsetting canonicalizes selector order; pre-sort and unique selectors to silence this warning.",
      call. = FALSE
    )
  }
  sorted
}

#' @rdname sub-.flexseq
#' @method [[ flexseq
#' @return For `[[`: the extracted element (internal name metadata is removed).
#' @examples
#'
#' # [[ extracts one element
#' x <- as_flexseq(letters[1:5])
#' x[[3]]
#'
#' x2 <- as_flexseq(setNames(as.list(letters[1:3]), c("a1", "a2", "a3")))
#' x2[["a2"]]
#' @export
# Runtime: O(log n) for integer lookup; O(n) for name lookup.
`[[.flexseq` <- function(x, i, ...) {
  if(is.character(i) && length(i) == 1L && !is.na(i)) {
    pos <- .ft_match_name_indices(x, i, strict_missing = TRUE)
    return(.ft_strip_name(.ft_get_elem_at(x, pos)))
  }
  n <- .ft_size(x)
  idx <- .ft_assert_int_indices(i, n)
  if(length(idx) != 1L) {
    stop("[[ expects exactly one index.")
  }

  .ft_strip_name(.ft_get_elem_at(x, idx))
}

#' @rdname sub-.flexseq
#' @method [<- flexseq
#' @param value Replacement values; recycled to selected index length.
#' @return For `[<-`: a new `flexseq` with selected elements replaced.
#' @examples
#'
#' # [<- replaces selected elements
#' x <- as_flexseq(1:6)
#' x
#'
#' x2 <- x
#' x2[c(2, 5)] <- list(20, 50)
#' x2
#'
#' # character replacement uses element names
#' x3 <- as_flexseq(setNames(as.list(1:4), c("a", "b", "c", "d")))
#' x3[c("d", "a")] <- list(40, 10)
#' x3
#'
#' # logical replacement supports recycling
#' x4 <- x
#' x4[c(TRUE, FALSE, TRUE, FALSE, TRUE, FALSE)] <- list(1)
#' x4
#' @export
# Runtime: sparse replacement path O(k * n) via repeated point updates; dense
# path O(n + k) via flatten + single rebuild, where n = tree size and
# k = number of replaced positions.
`[<-.flexseq` <- function(x, i, value) {
  ms <- resolve_tree_monoids(x, required = TRUE)
  vals <- as.list(value)
  n <- as.integer(node_measure(x, ".size"))

  if(is.logical(i)) {
    mask <- .ft_assert_lgl_indices(i, n)
    idx <- .ft_true_positions(mask)
    vals2 <- .ft_recycle_values(vals, length(idx))
    return(`[<-.flexseq`(x, idx, vals2))
  }

  if(is.character(i)) {
    idx <- .ft_assert_chr_indices(i)
    vals <- .ft_recycle_values(vals, length(idx))
    if(length(idx) == 0L) {
      return(x)
    }
    xs <- .ft_to_list(x)
    has_name_hints <- .ft_values_have_name_hints(vals)
    if(!has_name_hints) {
      pos <- .ft_match_name_indices(x, idx, strict_missing = TRUE)
      for(k in seq_along(pos)) {
        xs[pos[[k]]] <- list(.ft_set_name(vals[[k]], idx[[k]]))
      }
      return(.ft_restore_subclass(tree_from(xs, monoids = ms), x, context = "[<-"))
    }

    name_to_pos <- .ft_name_positions_fast(x)
    pos <- unname(name_to_pos[idx])
    if(any(is.na(pos))) {
      missing_names <- unique(idx[is.na(pos)])
      stop("Unknown element name(s): ", paste(missing_names, collapse = ", "))
    }
    pos <- as.integer(pos)
    vn <- names(vals)
    name_vec <- names(name_to_pos)
    for(k in seq_along(pos)) {
      name_hint <- if(!is.null(vn)) vn[[k]] else NULL
      nm <- .ft_effective_name(vals[[k]], name_hint)
      if(is.null(nm)) {
        nm <- idx[[k]]
      }
      upd <- .ft_update_name_map(name_to_pos, name_vec, pos[[k]], nm)
      name_to_pos <- upd$name_to_pos
      name_vec <- upd$name_vec
      xs[pos[[k]]] <- list(.ft_set_name(vals[[k]], nm))
    }
    return(.ft_restore_subclass(tree_from(xs, monoids = ms), x, context = "[<-"))
  }

  idx <- .ft_assert_int_indices(i, n)
  vals <- .ft_recycle_values(vals, length(idx))
  if(length(idx) == 0L) {
    return(x)
  }
  vn <- names(vals)
  has_name_hints <- .ft_values_have_name_hints(vals)
  n_named <- as.integer(node_measure(x, ".named_count"))
  use_name_map <- n > 0L && n_named == n

  # Sparse vector replacement is usually faster as repeated point updates.
  # Dense replacement is faster as flatten+single rebuild.
  sparse_cutoff <- max(8L, as.integer(sqrt(max(1L, n))))
  if(length(idx) <= sparse_cutoff) {
    out <- x
    if(use_name_map && has_name_hints) {
      name_to_pos <- .ft_name_positions_fast(x)
      name_vec <- names(name_to_pos)
    }
    for(k in seq_along(idx)) {
      name_hint <- if(!is.null(vn)) vn[[k]] else NULL
      nm <- .ft_effective_name(vals[[k]], name_hint)
      if(is.null(nm)) {
        old <- .ft_get_elem_at(out, idx[[k]])
        nm <- .ft_get_name(old)
      }
      if(use_name_map && has_name_hints && !is.null(nm)) {
        upd <- .ft_update_name_map(name_to_pos, name_vec, idx[[k]], nm)
        name_to_pos <- upd$name_to_pos
        name_vec <- upd$name_vec
      }
      out[[idx[[k]]]] <- .ft_set_name(vals[[k]], nm)
    }
    return(.ft_restore_subclass(out, x, context = "[<-"))
  }

  xs <- .ft_to_list(x)
  if(use_name_map && has_name_hints) {
    name_to_pos <- .ft_name_positions_fast(x)
    name_vec <- names(name_to_pos)
  }
  for(k in seq_along(idx)) {
    name_hint <- if(!is.null(vn)) vn[[k]] else NULL
    nm <- .ft_effective_name(vals[[k]], name_hint)
    if(is.null(nm)) {
      old <- xs[[idx[[k]]]]
      nm <- .ft_get_name(old)
    }
    if(use_name_map && has_name_hints && !is.null(nm)) {
      upd <- .ft_update_name_map(name_to_pos, name_vec, idx[[k]], nm)
      name_to_pos <- upd$name_to_pos
      name_vec <- upd$name_vec
    }
    v <- .ft_set_name(vals[[k]], nm)
    xs[idx[[k]]] <- list(v)
  }
  .ft_restore_subclass(tree_from(xs, monoids = ms), x, context = "[<-")
}

#' @rdname sub-.flexseq
#' @method [[<- flexseq
#' @return For `[[<-`: a new `flexseq` with one element replaced.
#' @examples
#'
#' # [[<- replaces one element
#' x <- as_flexseq(letters[1:4])
#' x2 <- x
#' x2[[2]] <- "ZZ"
#' x2
#'
#' x3 <- as_flexseq(setNames(as.list(1:3), c("x", "y", "z")))
#' x3[["y"]] <- 99
#' x3
#'
#' # assigning NULL removes one element
#' x4 <- as_flexseq(letters[1:4])
#' x4[[2]] <- NULL
#' x4
#' @export
# Runtime: O(log n) via split + push_back + concat.
`[[<-.flexseq` <- function(x, i, value) {
  if(is.character(i) && length(i) == 1L && !is.na(i)) {
    pos <- .ft_match_name_indices(x, i, strict_missing = TRUE)
    if(is.null(value)) {
      return(`[[<-.flexseq`(x, pos, NULL))
    }
    nm <- .ft_effective_name(value)
    if(is.null(nm)) {
      nm <- i
    }
    v <- .ft_set_name(value, nm)
    return(`[[<-.flexseq`(x, pos, v))
  }
  ms <- attr(x, "monoids", exact = TRUE)
  if(is.null(ms)) stop("Tree has no monoids attribute.")
  n <- .ft_size(x)
  idx <- .ft_assert_int_indices(i, n)
  if(length(idx) != 1L) {
    stop("[[<- expects exactly one index.")
  }
  use_cpp <- .ft_cpp_can_use(ms)
  .split_here <- if(use_cpp) {
    function(i) .ft_cpp_split_at_index(x, i, ms)
  } else {
    function(i) split_around_by_predicate(x, function(v) v >= i, ".size")
  }
  if(is.null(value)) {
    s <- .split_here(idx)
    concat_out <- if(use_cpp) .ft_cpp_concat(s$left, s$right, ms) else .ft_concat_same_monoids(s$left, s$right, ms)
    return(.ft_restore_subclass(concat_out, x, context = "[[<-"))
  }
  n_named <- .ft_named_count(x)
  nm <- .ft_get_name_fast(value)
  if(is.null(nm)) nm <- .ft_name_from_value(value)
  old <- NULL
  if(is.null(nm) && n_named > 0L) {
    old <- .ft_get_elem_at(x, idx)
    nm <- .ft_get_name_fast(old)
  }
  if(n > 0L && n_named == n && !is.null(nm)) {
    if(is.null(old)) {
      old <- .ft_get_elem_at(x, idx)
    }
    old_nm <- .ft_get_name_fast(old)
    if(is.null(old_nm)) {
      stop("Invalid name state: mixed named and unnamed elements are not allowed.")
    }
    if(!identical(nm, old_nm)) {
      existing <- if(.ft_cpp_enabled()) .ft_cpp_find_name_position(x, nm) else .ft_find_name_position(x, nm)
      if(!is.na(existing) && as.integer(existing) != as.integer(idx)) {
        stop("Element names must be unique.")
      }
    }
  }
  value <- .ft_set_name_fast(value, nm)

  s <- .split_here(idx)
  left_plus <- .ft_push_back_raw(s$left, value, ms)
  concat_out <- if(use_cpp) .ft_cpp_concat(left_plus, s$right, ms) else .ft_concat_same_monoids(left_plus, s$right, ms)
  .ft_restore_subclass(concat_out, x, context = "[[<-")
}

Try the Immutables package in your browser

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

Immutables documentation built on April 29, 2026, 1:06 a.m.