From 096d2f5177fc5d86eea16f907052c72b48fc4f7f Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 14:57:11 +0200 Subject: [PATCH 1/9] perf: lazy ALTREP names for vertex/edge sequences Vertex/edge sequences eagerly materialized their `names` (and edge `vnames`) character vectors at construction time. For functions that return many sequences (e.g. max_cliques returning tens of thousands), this allocates a named character vector per object even when the names are never read. Add an `igraph_lazy_names` ALTREP string class (src/rinterface_extra.c) that wraps the graph's full name vector plus a 1-based index, and only materializes the strings when an element is touched (printing, named indexing, as_ids()). Subsetting stays lazy via an Extract_subset method that composes indices. V() and E() now attach names through the lazy_index_names() helper instead of subsetting eagerly; downstream constructors (create_vs/simple_vs_index/...) inherit laziness through the Extract_subset path with no changes. Names resolve identically to before; vertex/edge sequence printing, named indexing and as_ids() are unaffected. Note: this removes the name-materialization penalty (named sequences now cost about the same as unnamed), but it is not by itself sufficient to close the gap to the numeric (return.vs.es = FALSE) path -- that is dominated by per-object graph-reference attachment in add_vses_graph_ref(), addressed separately. cpp11::cpp_register() also drops two dead registrations (Rx_igraph_weak_ref_run_finalizer, UUID_gen) that no R code .Call()s. Co-Authored-By: Claude Opus 4.8 (1M context) --- R/iterators.R | 20 ++++++- src/cpp11.cpp | 6 +- src/rinterface_extra.c | 123 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 6 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index 78d20e0d1a4..1a8e4fddded 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -87,6 +87,22 @@ identical_graphs <- function(g1, g2, attrs = TRUE) { .Call(Rx_igraph_identical_graphs, g1, g2, as.logical(attrs)) } +# Build the `names` attribute of a vertex/edge sequence lazily. +# +# `source` is the graph's full vertex/edge name vector (shared by reference +# across all sequences of the graph) and `idx` is a 1-based index into it. +# The result is an ALTREP string vector that only materializes the actual +# names when they are first accessed (printing, named indexing, `as_ids()`), +# so constructing a sequence stays cheap even when many of them are returned +# at once (e.g. `max_cliques()`). Subsetting it stays lazy too, via the +# class's `Extract_subset` method. Returns `NULL` when there is no source. +lazy_index_names <- function(source, idx) { + if (is.null(source)) { + return(NULL) + } + .Call(Rx_igraph_lazy_names, source, idx) +} + add_vses_graph_ref <- function(vses, graph) { ref <- get_vs_ref(graph) if (!is.null(ref)) { @@ -253,7 +269,7 @@ V <- function(graph) { res <- seq_len(vcount(graph)) if (is_named(graph)) { - names(res) <- vertex_attr(graph)$name + names(res) <- lazy_index_names(vertex_attr(graph)$name, res) } class(res) <- "igraph.vs" res <- set_complete_iterator(res) @@ -381,7 +397,7 @@ E <- function(graph, P = NULL, path = NULL, directed = TRUE) { } if ("name" %in% edge_attr_names(graph)) { - names(res) <- edge_attr(graph)$name[res] + names(res) <- lazy_index_names(edge_attr(graph)$name, res) } if (is_named(graph)) { el <- ends(graph, es = res) diff --git a/src/cpp11.cpp b/src/cpp11.cpp index dfbdf12f4fc..f193ae2ac9a 100644 --- a/src/cpp11.cpp +++ b/src/cpp11.cpp @@ -564,6 +564,7 @@ extern SEXP Rx_igraph_layout_kamada_kawai_3d(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, extern SEXP Rx_igraph_layout_lgl(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_layout_merge_dla(SEXP, SEXP); extern SEXP Rx_igraph_layout_reingold_tilford(SEXP, SEXP, SEXP, SEXP, SEXP); +extern SEXP Rx_igraph_lazy_names(SEXP, SEXP); extern SEXP Rx_igraph_make_weak_ref(SEXP, SEXP, SEXP); extern SEXP Rx_igraph_maximal_cliques(SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_maximal_cliques_count(SEXP, SEXP, SEXP, SEXP); @@ -596,12 +597,10 @@ extern SEXP Rx_igraph_vs_adj(SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_vs_nei(SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_walktrap_community(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_weak_ref_key(SEXP); -extern SEXP Rx_igraph_weak_ref_run_finalizer(SEXP); extern SEXP Rx_igraph_weak_ref_value(SEXP); extern SEXP Rx_igraph_write_graph_dimacs(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_write_graph_lgl(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_write_graph_ncol(SEXP, SEXP, SEXP, SEXP); -extern SEXP UUID_gen(SEXP); static const R_CallMethodDef CallEntries[] = { {"R_igraph_add_edge", (DL_FUNC) &R_igraph_add_edge, 3}, @@ -1146,6 +1145,7 @@ static const R_CallMethodDef CallEntries[] = { {"Rx_igraph_layout_lgl", (DL_FUNC) &Rx_igraph_layout_lgl, 8}, {"Rx_igraph_layout_merge_dla", (DL_FUNC) &Rx_igraph_layout_merge_dla, 2}, {"Rx_igraph_layout_reingold_tilford", (DL_FUNC) &Rx_igraph_layout_reingold_tilford, 5}, + {"Rx_igraph_lazy_names", (DL_FUNC) &Rx_igraph_lazy_names, 2}, {"Rx_igraph_make_weak_ref", (DL_FUNC) &Rx_igraph_make_weak_ref, 3}, {"Rx_igraph_maximal_cliques", (DL_FUNC) &Rx_igraph_maximal_cliques, 4}, {"Rx_igraph_maximal_cliques_count", (DL_FUNC) &Rx_igraph_maximal_cliques_count, 4}, @@ -1178,12 +1178,10 @@ static const R_CallMethodDef CallEntries[] = { {"Rx_igraph_vs_nei", (DL_FUNC) &Rx_igraph_vs_nei, 4}, {"Rx_igraph_walktrap_community", (DL_FUNC) &Rx_igraph_walktrap_community, 6}, {"Rx_igraph_weak_ref_key", (DL_FUNC) &Rx_igraph_weak_ref_key, 1}, - {"Rx_igraph_weak_ref_run_finalizer", (DL_FUNC) &Rx_igraph_weak_ref_run_finalizer, 1}, {"Rx_igraph_weak_ref_value", (DL_FUNC) &Rx_igraph_weak_ref_value, 1}, {"Rx_igraph_write_graph_dimacs", (DL_FUNC) &Rx_igraph_write_graph_dimacs, 5}, {"Rx_igraph_write_graph_lgl", (DL_FUNC) &Rx_igraph_write_graph_lgl, 5}, {"Rx_igraph_write_graph_ncol", (DL_FUNC) &Rx_igraph_write_graph_ncol, 4}, - {"UUID_gen", (DL_FUNC) &UUID_gen, 1}, {"_igraph_getsphere", (DL_FUNC) &_igraph_getsphere, 7}, {"_igraph_igraph_hcass2", (DL_FUNC) &_igraph_igraph_hcass2, 3}, {NULL, NULL, 0} diff --git a/src/rinterface_extra.c b/src/rinterface_extra.c index e616295c64f..bf1e243a28d 100644 --- a/src/rinterface_extra.c +++ b/src/rinterface_extra.c @@ -2501,6 +2501,122 @@ static void *Rx_igraph_altrep_to(SEXP vec, Rboolean writeable) { static R_altrep_class_t Rx_igraph_altrep_from_class; static R_altrep_class_t Rx_igraph_altrep_to_class; +/* ------------------------------------------------------------------------ + * Lazy names for vertex/edge sequences. + * + * A vertex/edge sequence carries its `names` attribute as an instance of this + * ALTREP string class instead of a materialized character vector. The actual + * names are only built when an element is touched (printing, named indexing, + * as_ids()), not when the sequence is constructed -- which is the common case + * for functions that return tens of thousands of sequences (e.g. max_cliques). + * + * data1 = list(source, idx): `source` is the graph's full vertex/edge name + * vector (shared by reference across all sequences of a graph) and `idx` is a + * 1-based integer index into `source`. data2 caches the materialized STRSXP. + * ------------------------------------------------------------------------ */ +static R_altrep_class_t Rx_igraph_lazy_names_class; + +static R_xlen_t Rx_igraph_lazy_names_length(SEXP vec) { + return XLENGTH(VECTOR_ELT(R_altrep_data1(vec), 1)); +} + +static SEXP Rx_igraph_lazy_names_materialize(SEXP vec) { + SEXP data=R_altrep_data2(vec); + if (data != R_NilValue) { + return data; + } + + SEXP d1=R_altrep_data1(vec); + SEXP source=VECTOR_ELT(d1, 0); + SEXP idx=VECTOR_ELT(d1, 1); + R_xlen_t n=XLENGTH(idx); + R_xlen_t nsource=XLENGTH(source); + const int *pidx=INTEGER(idx); + + PROTECT(data=Rf_allocVector(STRSXP, n)); + for (R_xlen_t i=0; i < n; i++) { + int j=pidx[i]; + if (j == NA_INTEGER || j < 1 || j > nsource) { + SET_STRING_ELT(data, i, NA_STRING); + } else { + SET_STRING_ELT(data, i, STRING_ELT(source, j - 1)); + } + } + R_set_altrep_data2(vec, data); + UNPROTECT(1); + return data; +} + +static void *Rx_igraph_lazy_names_dataptr(SEXP vec, Rboolean writeable) { + return DATAPTR(Rx_igraph_lazy_names_materialize(vec)); +} + +static const void *Rx_igraph_lazy_names_dataptr_or_null(SEXP vec) { + SEXP data=R_altrep_data2(vec); + if (data == R_NilValue) { + return NULL; + } + return DATAPTR_OR_NULL(data); +} + +static SEXP Rx_igraph_lazy_names_elt(SEXP vec, R_xlen_t i) { + return STRING_ELT(Rx_igraph_lazy_names_materialize(vec), i); +} + +/* Subsetting stays lazy: return a fresh lazy-names vector with composed + * indices instead of materializing. Falls back to the default (materialize) + * for index types we do not handle here by returning NULL. */ +static SEXP Rx_igraph_lazy_names_extract_subset(SEXP vec, SEXP indx, SEXP call) { + if (TYPEOF(indx) != INTSXP && TYPEOF(indx) != REALSXP) { + return NULL; + } + + SEXP d1=R_altrep_data1(vec); + SEXP source=VECTOR_ELT(d1, 0); + SEXP idx=VECTOR_ELT(d1, 1); + R_xlen_t leni=XLENGTH(idx); + const int *pidx=INTEGER(idx); + + SEXP indx_int=PROTECT(Rf_coerceVector(indx, INTSXP)); + R_xlen_t n=XLENGTH(indx_int); + const int *pind=INTEGER(indx_int); + + SEXP new_idx=PROTECT(Rf_allocVector(INTSXP, n)); + int *pnew=INTEGER(new_idx); + for (R_xlen_t k=0; k < n; k++) { + int p=pind[k]; + if (p == NA_INTEGER || p < 1 || p > leni) { + pnew[k]=NA_INTEGER; + } else { + pnew[k]=pidx[p - 1]; + } + } + + SEXP d1n=PROTECT(Rf_allocVector(VECSXP, 2)); + SET_VECTOR_ELT(d1n, 0, source); + SET_VECTOR_ELT(d1n, 1, new_idx); + SEXP res=R_new_altrep(Rx_igraph_lazy_names_class, d1n, R_NilValue); + UNPROTECT(3); + return res; +} + +/* Construct a lazy-names vector from a character `source` and a (1-based) + * integer `idx`. Returns R_NilValue when `source` is not usable, so callers + * can fall back to no names. */ +SEXP Rx_igraph_lazy_names(SEXP source, SEXP idx) { + if (TYPEOF(source) != STRSXP) { + return R_NilValue; + } + + SEXP idx_int=PROTECT(Rf_coerceVector(idx, INTSXP)); + SEXP d1=PROTECT(Rf_allocVector(VECSXP, 2)); + SET_VECTOR_ELT(d1, 0, source); + SET_VECTOR_ELT(d1, 1, idx_int); + SEXP res=R_new_altrep(Rx_igraph_lazy_names_class, d1, R_NilValue); + UNPROTECT(2); + return res; +} + void Rx_igraph_init_vector_class(DllInfo *dll) { Rx_igraph_altrep_from_class=R_make_altreal_class("igraph_from", "base", dll); Rx_igraph_altrep_to_class=R_make_altreal_class("igraph_to", "base", dll); @@ -2510,6 +2626,13 @@ void Rx_igraph_init_vector_class(DllInfo *dll) { R_set_altrep_Length_method(Rx_igraph_altrep_to_class, Rx_igraph_altrep_length); R_set_altvec_Dataptr_method(Rx_igraph_altrep_to_class, Rx_igraph_altrep_to); + + Rx_igraph_lazy_names_class=R_make_altstring_class("igraph_lazy_names", "igraph", dll); + R_set_altrep_Length_method(Rx_igraph_lazy_names_class, Rx_igraph_lazy_names_length); + R_set_altvec_Dataptr_method(Rx_igraph_lazy_names_class, Rx_igraph_lazy_names_dataptr); + R_set_altvec_Dataptr_or_null_method(Rx_igraph_lazy_names_class, Rx_igraph_lazy_names_dataptr_or_null); + R_set_altvec_Extract_subset_method(Rx_igraph_lazy_names_class, Rx_igraph_lazy_names_extract_subset); + R_set_altstring_Elt_method(Rx_igraph_lazy_names_class, Rx_igraph_lazy_names_elt); } void Rx_igraph_init_handlers(DllInfo *dll) { From 318ae260cd171caa8e0df86cfbc40e2b677dc817 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 14:59:10 +0200 Subject: [PATCH 2/9] test: add touchstone benchmarks for vertex/edge sequence construction Add a benchmark group exercising the sequence-construction path on named graphs, where building the `names`/`vnames` attribute and attaching the graph reference dominate: max_cliques (tens of thousands of vertex sequences), head_of over every edge, and V()/E() on a large named graph. These guard the lazy-names work and any future construction-path changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- touchstone/script.R | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/touchstone/script.R b/touchstone/script.R index 1d8dc2eb4a1..3199455e828 100644 --- a/touchstone/script.R +++ b/touchstone/script.R @@ -177,5 +177,58 @@ benchmark_run( n = 20 ) +# --------------------------------------------------------------------------- +# Group #5 - vertex/edge sequence construction on named graphs +# Functions that return (many) vertex/edge sequences pay for building the +# `names`/`vnames` attribute and attaching a graph reference to every object. +# These benchmarks exercise that construction path on *named* graphs, where +# the cost is highest. `max_cliques()` is the canonical case: it returns tens +# of thousands of vertex sequences, one per clique. +# --------------------------------------------------------------------------- +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnp(200L, 0.16, directed = FALSE) + V(g)$name <- paste0("v", seq_len(gorder(g))) + }, + max_cliques_named = max_cliques(g), + n = 20 +) + +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(1000L, 5000L) + V(g)$name <- paste0("v", seq_len(1000L)) + es <- E(g) + }, + head_of_named = head_of(g, es), + n = 20 +) + +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(20000L, 50000L) + V(g)$name <- paste0("v", seq_len(20000L)) + }, + V_named = V(g), + n = 20 +) + +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(20000L, 50000L) + V(g)$name <- paste0("v", seq_len(20000L)) + }, + E_named = E(g), + n = 20 +) + # Create the artifacts consumed by the GitHub Action. benchmark_analyze() From 7a0df7bfecf88c510806c6c3e11ffdd15b8dce0d Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 16:37:50 +0200 Subject: [PATCH 3/9] perf: share the graph weak-reference across constructed sequences Constructing a vertex/edge sequence attached a graph reference per object via add_vses_graph_ref(), which calls .Call(Rx_igraph_copy_env), .Call(Rx_igraph_make_weak_ref) and .Call(Rx_igraph_get_graph_id) for every object. For functions returning many sequences (e.g. max_cliques returning tens of thousands) this dominated construction time -- profiling showed it was ~75% of the cost, far more than name building. The weak reference's key is the graph's environment, which is identical for every sequence of a graph (Rf_duplicate() is a no-op on an environment, so get_vs_ref() returns the same env each call). A single shared weak reference is therefore semantically identical to one per object: while the graph is alive the reference resolves, and once the graph is released the (weak) reference reports it gone -- verified that get_vs_graph() still returns NULL after rm(graph); gc(). simple_es_index() already propagates env/graph from its input, so edge construction only needed the redundant per-object add_vses_graph_ref() call removed. simple_vs_index() now propagates env/graph the same way, and unsafe_create_vs()/unsafe_create_es() rely on that propagation. The existing `lapply(res, unsafe_create_vs, graph = graph, verts = V(graph))` call sites then share the single weak reference built by V()/E() with no call-site or codegen changes. max_cliques(sample_gnp(500, 0.15)) on a named graph: ~338ms -> ~200ms. Co-Authored-By: Claude Opus 4.8 (1M context) --- R/iterators.R | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index 1a8e4fddded..8f20a9a4898 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -292,8 +292,9 @@ unsafe_create_vs <- function(graph, idx, verts = NULL) { if (is.null(verts)) { verts <- V(graph) } - res <- simple_vs_index(verts, idx, na_ok = TRUE) - add_vses_graph_ref(res, graph) + # `simple_vs_index()` carries the graph reference over from `verts`, so the + # weak reference built once by `V(graph)` is shared across every call here. + simple_vs_index(verts, idx, na_ok = TRUE) } # Internal function to quickly convert integer vectors to igraph.es @@ -304,8 +305,9 @@ unsafe_create_es <- function(graph, idx, es = NULL) { if (is.null(es)) { es <- E(graph) } - res <- simple_es_index(es, idx, na_ok = TRUE) - add_vses_graph_ref(res, graph) + # `simple_es_index()` already carries the graph reference over from `es`, + # so the weak reference built once by `E(graph)` is shared across calls. + simple_es_index(es, idx, na_ok = TRUE) } @@ -420,6 +422,12 @@ simple_vs_index <- function(x, i, na_ok = FALSE) { if (!na_ok && anyNA(res)) { cli::cli_abort("Unknown vertex selected.") } + # Carry the graph reference over from `x`, mirroring `simple_es_index()`. + # All sequences derived from the same `x` (e.g. a single `V(graph)` reused + # across an lapply()) then share its weak reference instead of each minting + # a fresh one, which is the dominant cost when constructing many sequences. + attr(res, "env") <- attr(x, "env") + attr(res, "graph") <- attr(x, "graph") class(res) <- "igraph.vs" res } From 40334c6b7789bde00ef464634ac6a0466f37669d Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 16:43:57 +0200 Subject: [PATCH 4/9] perf: build sequence payload directly and set attributes in one pass Two construction-cost reductions on top of the shared weak reference: * simple_vs_index() now sets names/class/env/graph in a single `attributes<-` call instead of separate `attr<-`/`class<-` assignments. Each incremental assignment shallow-copies the vector, and that copying dominated when building many sequences. * unsafe_create_vs() no longer routes through simple_vs_index(). Its `idx` are vertex IDs from C and `verts` is always the full V(graph), so `verts[idx]` just reproduces `idx`; we now use the IDs directly as the (integer) payload and take names lazily from `verts` via the ALTREP's O(1) subsetting, avoiding a full copy of V(graph) per object. Payload type (integer), names, NA handling and graph recovery are unchanged. max_cliques(sample_gnp(500, 0.15)) on a named graph: ~200ms -> ~140ms (was ~338ms before this branch). Co-Authored-By: Claude Opus 4.8 (1M context) --- R/iterators.R | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index 8f20a9a4898..af98b7ba809 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -292,9 +292,22 @@ unsafe_create_vs <- function(graph, idx, verts = NULL) { if (is.null(verts)) { verts <- V(graph) } - # `simple_vs_index()` carries the graph reference over from `verts`, so the - # weak reference built once by `V(graph)` is shared across every call here. - simple_vs_index(verts, idx, na_ok = TRUE) + # `idx` are vertex IDs straight from C, and `verts` is the full `V(graph)`, + # so `verts[idx]` would just be `idx` again -- skip that copy and use the + # IDs directly as the payload. Names are taken lazily from `verts` (an + # ALTREP that composes in O(1) under subsetting), and the graph reference is + # shared from `verts`. All attributes are set in one `attributes<-` call to + # avoid the per-object shallow copies that dominate when many sequences are + # built (e.g. `max_cliques()`). + nms <- attr(verts, "names") + res <- as.integer(idx) + attributes(res) <- list( + names = if (is.null(nms)) NULL else nms[idx], + class = "igraph.vs", + env = attr(verts, "env"), + graph = attr(verts, "graph") + ) + res } # Internal function to quickly convert integer vectors to igraph.es @@ -422,13 +435,19 @@ simple_vs_index <- function(x, i, na_ok = FALSE) { if (!na_ok && anyNA(res)) { cli::cli_abort("Unknown vertex selected.") } - # Carry the graph reference over from `x`, mirroring `simple_es_index()`. - # All sequences derived from the same `x` (e.g. a single `V(graph)` reused - # across an lapply()) then share its weak reference instead of each minting - # a fresh one, which is the dominant cost when constructing many sequences. - attr(res, "env") <- attr(x, "env") - attr(res, "graph") <- attr(x, "graph") - class(res) <- "igraph.vs" + # Set every attribute in a single `attributes<-` call rather than one + # `attr<-`/`class<-` at a time: each incremental assignment shallow-copies + # the vector, and that copying dominates when many sequences are built + # (e.g. `max_cliques()`). `names` is carried over from the subset above (a + # lazy ALTREP, or NULL); env/graph are carried from `x`, mirroring + # `simple_es_index()`, so sequences derived from one `V(graph)` share its + # weak reference instead of each minting a fresh one. + attributes(res) <- list( + names = attr(res, "names"), + class = "igraph.vs", + env = attr(x, "env"), + graph = attr(x, "graph") + ) res } From 53f1a7ca759713dc6773a3bbb0e1756c08056501 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 20:30:46 +0200 Subject: [PATCH 5/9] perf: batch-construct vertex sequence lists via create_vs_list() Functions returning many vertex sequences used `lapply(res, unsafe_create_vs, graph = graph, verts = V(graph))`, which re-read the graph reference, graph id and name source from `verts` on every object. Add `create_vs_list(graph, idx_list)` which hoists all of that per-graph work out of the loop and builds each sequence with a single `attributes<-` and a directly-constructed lazy-names ALTREP, so per-object cost drops to ~an integer coercion plus attribute set. The `VERTEXSET_LIST` OUTCONV template in tools/stimulus/types-RR.yaml now emits `create_vs_list(graph, res)`; the generated R/aaa-*.R files are updated to match (27 sites), along with the hand-written call sites in cliques.R, cohesive.blocks.R, components.R, conversion.R, interface.R, paths.R and structural-properties.R (10 sites). `unsafe_create_vs()` stays as the single-object form. Edge sequences are left as-is: simple_es_index() already propagates the shared weak reference from a single E(graph), so unsafe_create_es() does not pay the per-object cost. A lazy `vnames` and an es batch form remain as follow-ups. max_cliques(sample_gnp(500, 0.15)) on a named graph: ~140ms -> ~100ms; ~338ms -> ~100ms over the whole branch (numeric floor ~25-30ms). Output, names, NA handling and graph recovery are unchanged; 0 test failures across the vertex-sequence-returning suites and aaa-auto. Co-Authored-By: Claude Opus 4.8 (1M context) --- R/aaa-cliques.R | 18 +++++++++--------- R/aaa-cycles.R | 2 +- R/aaa-flows.R | 6 +++--- R/aaa-graphlets.R | 4 ++-- R/aaa-isomorphism.R | 2 +- R/aaa-paths.R | 14 +++++++------- R/aaa-separators.R | 4 ++-- R/aaa-structural.R | 4 ++-- R/cliques.R | 4 ++-- R/cohesive.blocks.R | 7 +------ R/components.R | 7 +------ R/conversion.R | 2 +- R/interface.R | 2 +- R/iterators.R | 34 ++++++++++++++++++++++++++++++++++ R/paths.R | 2 +- R/structural-properties.R | 16 +++------------- tools/stimulus/types-RR.yaml | 2 +- 17 files changed, 72 insertions(+), 58 deletions(-) diff --git a/R/aaa-cliques.R b/R/aaa-cliques.R index 83f6472fdf4..42bcaf53a44 100644 --- a/R/aaa-cliques.R +++ b/R/aaa-cliques.R @@ -107,7 +107,7 @@ cliques_impl <- function( max ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -163,7 +163,7 @@ largest_cliques_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -303,7 +303,7 @@ maximal_cliques_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -338,7 +338,7 @@ maximal_cliques_subset_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res$res <- lapply(res$res, unsafe_create_vs, graph = graph, verts = V(graph)) + res$res <- create_vs_list(graph, res$res) } if (!details) { res <- res$res @@ -383,7 +383,7 @@ independent_vertex_sets_impl <- function( max_size ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -420,7 +420,7 @@ largest_independent_vertex_sets_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -438,7 +438,7 @@ maximal_independent_vertex_sets_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -468,7 +468,7 @@ largest_weighted_cliques_impl <- function( vertex_weights ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -531,7 +531,7 @@ weighted_cliques_impl <- function( maximal ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/aaa-cycles.R b/R/aaa-cycles.R index f7671c6bde0..c97622ce2f7 100644 --- a/R/aaa-cycles.R +++ b/R/aaa-cycles.R @@ -380,7 +380,7 @@ simple_cycles_impl <- function( max_cycle_length ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) diff --git a/R/aaa-flows.R b/R/aaa-flows.R index 44f5b8faf0a..e01723400b3 100644 --- a/R/aaa-flows.R +++ b/R/aaa-flows.R @@ -17,7 +17,7 @@ cohesive_blocks_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res$blocks <- lapply(res$blocks, unsafe_create_vs, graph = graph, verts = V(graph)) + res$blocks <- create_vs_list(graph, res$blocks) } class(res) <- "cohesiveBlocks" res @@ -176,7 +176,7 @@ all_st_cuts_impl <- function( res$cuts <- lapply(res$cuts, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$partition1s <- lapply(res$partition1s, unsafe_create_vs, graph = graph, verts = V(graph)) + res$partition1s <- create_vs_list(graph, res$partition1s) } res } @@ -225,7 +225,7 @@ all_st_mincuts_impl <- function( res$cuts <- lapply(res$cuts, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$partition1s <- lapply(res$partition1s, unsafe_create_vs, graph = graph, verts = V(graph)) + res$partition1s <- create_vs_list(graph, res$partition1s) } res } diff --git a/R/aaa-graphlets.R b/R/aaa-graphlets.R index e9604885b0c..2a156b1c273 100644 --- a/R/aaa-graphlets.R +++ b/R/aaa-graphlets.R @@ -26,7 +26,7 @@ graphlets_candidate_basis_impl <- function( weights ) if (igraph_opt("return.vs.es")) { - res$cliques <- lapply(res$cliques, unsafe_create_vs, graph = graph, verts = V(graph)) + res$cliques <- create_vs_list(graph, res$cliques) } res } @@ -57,7 +57,7 @@ graphlets_impl <- function( niter ) if (igraph_opt("return.vs.es")) { - res$cliques <- lapply(res$cliques, unsafe_create_vs, graph = graph, verts = V(graph)) + res$cliques <- create_vs_list(graph, res$cliques) } res } diff --git a/R/aaa-isomorphism.R b/R/aaa-isomorphism.R index 343419a9d95..af52bd6da37 100644 --- a/R/aaa-isomorphism.R +++ b/R/aaa-isomorphism.R @@ -174,7 +174,7 @@ automorphism_group_impl <- function( sh ) if (igraph_opt("return.vs.es")) { - res$generators <- lapply(res$generators, unsafe_create_vs, graph = graph, verts = V(graph)) + res$generators <- create_vs_list(graph, res$generators) } if (!details) { res <- res$generators diff --git a/R/aaa-paths.R b/R/aaa-paths.R index 35daacbeb2d..95a29f097c4 100644 --- a/R/aaa-paths.R +++ b/R/aaa-paths.R @@ -784,7 +784,7 @@ get_all_shortest_paths_dijkstra_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -826,7 +826,7 @@ get_all_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -931,7 +931,7 @@ get_k_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply(res$vpaths, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vpaths <- create_vs_list(graph, res$vpaths) } if (igraph_opt("return.vs.es")) { res$epaths <- lapply(res$epaths, unsafe_create_es, graph = graph, es = E(graph)) @@ -1206,7 +1206,7 @@ get_shortest_paths_bellman_ford_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1258,7 +1258,7 @@ get_shortest_paths_dijkstra_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1300,7 +1300,7 @@ get_shortest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) @@ -1453,7 +1453,7 @@ get_widest_paths_impl <- function( mode ) if (igraph_opt("return.vs.es")) { - res$vertices <- lapply(res$vertices, unsafe_create_vs, graph = graph, verts = V(graph)) + res$vertices <- create_vs_list(graph, res$vertices) } if (igraph_opt("return.vs.es")) { res$edges <- lapply(res$edges, unsafe_create_es, graph = graph, es = E(graph)) diff --git a/R/aaa-separators.R b/R/aaa-separators.R index ef97067ea63..5705728e6a8 100644 --- a/R/aaa-separators.R +++ b/R/aaa-separators.R @@ -14,7 +14,7 @@ all_minimal_st_separators_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } @@ -86,7 +86,7 @@ minimum_size_separators_impl <- function( graph ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/aaa-structural.R b/R/aaa-structural.R index de9543e847b..e8adb9454ad 100644 --- a/R/aaa-structural.R +++ b/R/aaa-structural.R @@ -336,7 +336,7 @@ biconnected_components_impl <- function( res$component_edges <- lapply(res$component_edges, unsafe_create_es, graph = graph, es = E(graph)) } if (igraph_opt("return.vs.es")) { - res$components <- lapply(res$components, unsafe_create_vs, graph = graph, verts = V(graph)) + res$components <- create_vs_list(graph, res$components) } if (igraph_opt("return.vs.es")) { res$articulation_points <- create_vs(graph, res$articulation_points) @@ -1290,7 +1290,7 @@ neighborhood_impl <- function( mindist ) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/cliques.R b/R/cliques.R index 9005ce8f8cc..ff26cf0f022 100644 --- a/R/cliques.R +++ b/R/cliques.R @@ -358,7 +358,7 @@ max_cliques <- function( res <- lapply(res, function(x) x + 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res @@ -582,7 +582,7 @@ ivs <- function(graph, min = NULL, max = NULL) { res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res diff --git a/R/cohesive.blocks.R b/R/cohesive.blocks.R index b969a8fee76..e566e9dea6c 100644 --- a/R/cohesive.blocks.R +++ b/R/cohesive.blocks.R @@ -362,12 +362,7 @@ cohesive_blocks <- function(graph, labels = TRUE) { res$labels <- V(graph)$name } if (igraph_opt("return.vs.es")) { - res$blocks <- lapply( - res$blocks, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$blocks <- create_vs_list(graph, res$blocks) } res$vcount <- vcount(graph) diff --git a/R/components.R b/R/components.R index b4709d7a49a..dc502c78b0a 100644 --- a/R/components.R +++ b/R/components.R @@ -343,12 +343,7 @@ biconnected_components <- function(graph) { res$component.edges <- res$component_edges } if (igraph_opt("return.vs.es")) { - res$components <- lapply( - res$components, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$components <- create_vs_list(graph, res$components) } if (igraph_opt("return.vs.es")) { res$articulation_points <- create_vs(graph, res$articulation_points) diff --git a/R/conversion.R b/R/conversion.R index f6e6cd062c5..4af3dce0c42 100644 --- a/R/conversion.R +++ b/R/conversion.R @@ -658,7 +658,7 @@ as_adj_list <- function( res <- .Call(Rx_igraph_get_adjlist, graph, mode, loops, multiple) res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } if (is_named(graph)) { names(res) <- V(graph)$name diff --git a/R/interface.R b/R/interface.R index bc528bf78d0..8cbc5f21245 100644 --- a/R/interface.R +++ b/R/interface.R @@ -641,7 +641,7 @@ adjacent_vertices <- function(graph, v, mode = c("out", "in", "all", "total")) { res <- lapply(res, `+`, 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } if (is_named(graph)) { diff --git a/R/iterators.R b/R/iterators.R index af98b7ba809..9319a15c3ca 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -310,6 +310,40 @@ unsafe_create_vs <- function(graph, idx, verts = NULL) { res } +# Build a list of vertex sequences from a list of vertex-ID vectors. +# +# This is the batch form of `unsafe_create_vs()` and replaces the +# `lapply(idx_list, unsafe_create_vs, graph = graph, verts = V(graph))` +# pattern. The per-graph work -- `V(graph)`, the shared weak reference, the +# graph id and the lazy name source -- is hoisted out of the loop, so each +# sequence only costs one `as.integer()` and one `attributes<-`. This is what +# brings construction of many sequences (e.g. `max_cliques()`) down close to +# the cost of returning bare numeric indices. +create_vs_list <- function(graph, idx_list) { + verts <- V(graph) + envv <- attr(verts, "env") + gidd <- attr(verts, "graph") + src <- if (is_named(graph)) vertex_attr(graph)$name else NULL + if (is.null(src)) { + lapply(idx_list, function(idx) { + res <- as.integer(idx) + attributes(res) <- list(class = "igraph.vs", env = envv, graph = gidd) + res + }) + } else { + lapply(idx_list, function(idx) { + res <- as.integer(idx) + attributes(res) <- list( + names = .Call(Rx_igraph_lazy_names, src, res), + class = "igraph.vs", + env = envv, + graph = gidd + ) + res + }) + } +} + # Internal function to quickly convert integer vectors to igraph.es # for use after C code, when NA and bounds checking is unnecessary # Also allows us to construct V(graph) outside the function call in diff --git a/R/paths.R b/R/paths.R index bc0468425b9..0992ff06de7 100644 --- a/R/paths.R +++ b/R/paths.R @@ -130,7 +130,7 @@ all_simple_paths <- function( res <- get.all.simple.paths.pp(res) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res } diff --git a/R/structural-properties.R b/R/structural-properties.R index 9182950592e..8826ba1f9cd 100644 --- a/R/structural-properties.R +++ b/R/structural-properties.R @@ -1355,12 +1355,7 @@ shortest_paths <- function( if (igraph_opt("return.vs.es")) { if (!is.null(res$vpath)) { - res$vpath <- lapply( - res$vpath, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$vpath <- create_vs_list(graph, res$vpath) } if (!is.null(res$epath)) { res$epath <- lapply( @@ -1426,12 +1421,7 @@ all_shortest_paths <- function( } if (igraph_opt("return.vs.es")) { - res$vpaths <- lapply( - res$vpaths, - unsafe_create_vs, - graph = graph, - verts = V(graph) - ) + res$vpaths <- create_vs_list(graph, res$vpaths) } # Transitional, eventually, remove $res @@ -2181,7 +2171,7 @@ ego <- function( res <- lapply(res, function(x) x + 1) if (igraph_opt("return.vs.es")) { - res <- lapply(res, unsafe_create_vs, graph = graph, verts = V(graph)) + res <- create_vs_list(graph, res) } res diff --git a/tools/stimulus/types-RR.yaml b/tools/stimulus/types-RR.yaml index bfb5ca69ed8..3c9ac878978 100644 --- a/tools/stimulus/types-RR.yaml +++ b/tools/stimulus/types-RR.yaml @@ -472,7 +472,7 @@ VERTEXSET_LIST: OUTCONV: OUT: |- if (igraph_opt("return.vs.es")) { - %I% <- lapply(%I%, unsafe_create_vs, graph = %I1%, verts = V(%I1%)) + %I% <- create_vs_list(%I1%, %I%) } EDGESET_LIST: From b6e7fa30c693d09048140a18b01a24ceaf65fdc8 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 20:54:37 +0200 Subject: [PATCH 6/9] test: add touchstone benchmarks showcasing the construction speedups Add a benchmark group exercising the three distinct wins on named graphs: * ego_order2_named -- batch construction of one vertex sequence per node (create_vs_list via neighborhood()). * max_cliques_sizes_named -- build many cliques but only read their sizes; with lazy names the name vectors are never materialized. * all_simple_paths_named -- another high-volume vertex-sequence-list path. * vs_subset_positional -- O(1) lazy subsetting of a large named vertex sequence (ALTREP Extract_subset) instead of copying a name subset. Co-Authored-By: Claude Opus 4.8 (1M context) --- touchstone/script.R | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/touchstone/script.R b/touchstone/script.R index 3199455e828..d20a3a20993 100644 --- a/touchstone/script.R +++ b/touchstone/script.R @@ -230,5 +230,69 @@ benchmark_run( n = 20 ) +# --------------------------------------------------------------------------- +# Group #6 - what lazy names + batch construction buy +# These illustrate the three distinct wins on named graphs: +# * batch construction of many vertex sequences (one shared graph reference +# and one hoisted name source instead of per-object work); +# * lazy names that are never paid for when only the IDs/sizes are used; +# * O(1) lazy subsetting of a vertex sequence (ALTREP Extract_subset). +# --------------------------------------------------------------------------- + +# ego() returns one vertex sequence per node -- a few thousand sequences built +# in one call. Exercises create_vs_list() through neighborhood(). +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(2000L, 10000L) + V(g)$name <- paste0("v", seq_len(2000L)) + }, + ego_order2_named = ego(g, order = 2, nodes = V(g)), + n = 20 +) + +# Construct many cliques but only read their sizes: with lazy names the name +# vectors are never materialized at all, so this is close to the numeric path. +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnp(200L, 0.16, directed = FALSE) + V(g)$name <- paste0("v", seq_len(gorder(g))) + }, + max_cliques_sizes_named = lengths(max_cliques(g)), + n = 20 +) + +# Enumerate simple paths between hubs on a named graph: another high-volume +# vertex-sequence-list path (create_vs_list via all_simple_paths()). +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(500L, 2500L) + V(g)$name <- paste0("v", seq_len(500L)) + }, + all_simple_paths_named = all_simple_paths(g, 1, 2:6, cutoff = 5), + n = 20 +) + +# Positional subset of a large named vertex sequence. Subsetting a lazy-names +# sequence composes indices in O(1) (ALTREP Extract_subset) instead of copying +# a subset of the name vector. +benchmark_run( + expr_before_benchmark = { + library(igraph) + set.seed(42) + g <- sample_gnm(50000L, 100000L) + V(g)$name <- paste0("v", seq_len(50000L)) + v <- V(g) + pick <- sample(50000L, 10000L) + }, + vs_subset_positional = v[pick], + n = 20 +) + # Create the artifacts consumed by the GitHub Action. benchmark_analyze() From 21f3d4b293a99b059db3dc07f3e432bbe2930974 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 21:06:13 +0200 Subject: [PATCH 7/9] perf: build the whole vertex-sequence list in one C pass create_vs_list() drove the per-element work (lapply closure, as.integer, .Call to build the lazy-names ALTREP, and attributes<-) from R, repeated once per sequence -- the dominant cost when functions like max_cliques() return tens of thousands of igraph.vs objects. Move that loop into Rx_igraph_vs_list() in rinterface_extra.c, which reuses the file-static igraph_lazy_names ALTREP class directly. R now only builds V(graph) once (to mint the shared weak reference and graph id) and hands the pieces to C. Each element gets a fresh integer payload (guarded against coerceVector aliasing so the caller's vectors are never mutated), an optional lazy-names attribute, the shared env/graph attributes, and the igraph.vs class. max_cliques(sample_gnp(500, 0.15)) on a named graph (medians, 12 iters): vs: ~100ms -> 34.5ms (numeric floor ~30ms) Correctness verified: integer payloads with identical values/names, no names on unnamed graphs, NA/out-of-range IDs map to NA_STRING, inputs unmutated, and the shared weakref still releases the graph after rm()+gc(). Full suite passes (iterators, cliques, paths, components, flow, conversion, interface, structural-properties, cycles, attributes, aaa-auto); clean under gctorture. cpp11::cpp_register() added only the Rx_igraph_vs_list registration; it left the previously-noted unused entries untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- R/iterators.R | 32 +++++++++--------------- src/cpp11.cpp | 2 ++ src/rinterface_extra.c | 56 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 21 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index 9319a15c3ca..af0de8e3500 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -320,28 +320,18 @@ unsafe_create_vs <- function(graph, idx, verts = NULL) { # brings construction of many sequences (e.g. `max_cliques()`) down close to # the cost of returning bare numeric indices. create_vs_list <- function(graph, idx_list) { + # `verts <- V(graph)` is what mints the single shared weak reference and graph + # id; build it once and hand the pieces to C, which runs the per-element + # construction loop (payload coercion, lazy-names ALTREP, attribute setting) + # without any per-object R overhead. verts <- V(graph) - envv <- attr(verts, "env") - gidd <- attr(verts, "graph") - src <- if (is_named(graph)) vertex_attr(graph)$name else NULL - if (is.null(src)) { - lapply(idx_list, function(idx) { - res <- as.integer(idx) - attributes(res) <- list(class = "igraph.vs", env = envv, graph = gidd) - res - }) - } else { - lapply(idx_list, function(idx) { - res <- as.integer(idx) - attributes(res) <- list( - names = .Call(Rx_igraph_lazy_names, src, res), - class = "igraph.vs", - env = envv, - graph = gidd - ) - res - }) - } + .Call( + Rx_igraph_vs_list, + idx_list, + if (is_named(graph)) vertex_attr(graph)$name else NULL, + attr(verts, "env"), + attr(verts, "graph") + ) } # Internal function to quickly convert integer vectors to igraph.es diff --git a/src/cpp11.cpp b/src/cpp11.cpp index f193ae2ac9a..f7e3c51694b 100644 --- a/src/cpp11.cpp +++ b/src/cpp11.cpp @@ -594,6 +594,7 @@ extern SEXP Rx_igraph_transitivity_local_undirected_all(SEXP, SEXP); extern SEXP Rx_igraph_union(SEXP, SEXP); extern SEXP Rx_igraph_vcount(SEXP); extern SEXP Rx_igraph_vs_adj(SEXP, SEXP, SEXP, SEXP); +extern SEXP Rx_igraph_vs_list(SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_vs_nei(SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_walktrap_community(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP Rx_igraph_weak_ref_key(SEXP); @@ -1175,6 +1176,7 @@ static const R_CallMethodDef CallEntries[] = { {"Rx_igraph_union", (DL_FUNC) &Rx_igraph_union, 2}, {"Rx_igraph_vcount", (DL_FUNC) &Rx_igraph_vcount, 1}, {"Rx_igraph_vs_adj", (DL_FUNC) &Rx_igraph_vs_adj, 4}, + {"Rx_igraph_vs_list", (DL_FUNC) &Rx_igraph_vs_list, 4}, {"Rx_igraph_vs_nei", (DL_FUNC) &Rx_igraph_vs_nei, 4}, {"Rx_igraph_walktrap_community", (DL_FUNC) &Rx_igraph_walktrap_community, 6}, {"Rx_igraph_weak_ref_key", (DL_FUNC) &Rx_igraph_weak_ref_key, 1}, diff --git a/src/rinterface_extra.c b/src/rinterface_extra.c index bf1e243a28d..2965c7e52d2 100644 --- a/src/rinterface_extra.c +++ b/src/rinterface_extra.c @@ -2617,6 +2617,62 @@ SEXP Rx_igraph_lazy_names(SEXP source, SEXP idx) { return res; } +/* Batch constructor for a list of vertex sequences. + * + * Builds the whole `lapply(idx_list, unsafe_create_vs, ...)` result in one C + * pass: for each vertex-ID vector it produces a fresh integer payload, attaches + * a lazy-names ALTREP (when the graph is named), and sets the shared `env` + * weak reference, the `graph` id and the `igraph.vs` class. This keeps the + * per-object R overhead (closure call, `as.integer`, `.Call`, `attributes<-`) + * out of the loop entirely. + * + * idx_list : VECSXP of vertex-ID vectors (integer or double) + * names_src : graph's full vertex-name STRSXP, or NULL for unnamed graphs + * env : the shared weak reference (or env) to set as the "env" attr + * graph_id : graph id (character scalar), or NULL to skip the "graph" attr + */ +SEXP Rx_igraph_vs_list(SEXP idx_list, SEXP names_src, SEXP env, SEXP graph_id) { + R_xlen_t n=XLENGTH(idx_list); + int named=(TYPEOF(names_src) == STRSXP); + SEXP env_sym=Rf_install("env"); + SEXP graph_sym=Rf_install("graph"); + SEXP out=PROTECT(Rf_allocVector(VECSXP, n)); + SEXP cls=PROTECT(Rf_mkString("igraph.vs")); + + for (R_xlen_t i=0; i < n; i++) { + SEXP elt=VECTOR_ELT(idx_list, i); + /* Fresh, unshared integer payload: coerceVector returns its argument + * unchanged when the type already matches, so duplicate in that case to + * avoid mutating a caller-owned vector. */ + SEXP payload=PROTECT(Rf_coerceVector(elt, INTSXP)); + if (payload == elt) { + UNPROTECT(1); + payload=PROTECT(Rf_duplicate(elt)); + } + + if (named) { + SEXP d1=PROTECT(Rf_allocVector(VECSXP, 2)); + SET_VECTOR_ELT(d1, 0, names_src); + SET_VECTOR_ELT(d1, 1, payload); + SEXP nm=PROTECT(R_new_altrep(Rx_igraph_lazy_names_class, d1, R_NilValue)); + Rf_setAttrib(payload, R_NamesSymbol, nm); + UNPROTECT(2); + } + + Rf_setAttrib(payload, env_sym, env); + if (graph_id != R_NilValue) { + Rf_setAttrib(payload, graph_sym, graph_id); + } + Rf_setAttrib(payload, R_ClassSymbol, cls); + + SET_VECTOR_ELT(out, i, payload); + UNPROTECT(1); + } + + UNPROTECT(2); + return out; +} + void Rx_igraph_init_vector_class(DllInfo *dll) { Rx_igraph_altrep_from_class=R_make_altreal_class("igraph_from", "base", dll); Rx_igraph_altrep_to_class=R_make_altreal_class("igraph_to", "base", dll); From 3b88059eca4961d40ae7f7273d37ea1e3ce742b1 Mon Sep 17 00:00:00 2001 From: schochastics Date: Fri, 5 Jun 2026 19:24:22 +0000 Subject: [PATCH 8/9] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/27035085951 --- man/biconnected.components.Rd | 2 +- man/biconnected_components.Rd | 2 +- man/graphlet_basis.Rd | 2 +- man/graphlets.candidate.basis.Rd | 2 +- man/k_shortest_paths.Rd | 2 +- man/simple_cycles.Rd | 2 +- man/stCuts.Rd | 2 +- man/stMincuts.Rd | 2 +- man/st_cuts.Rd | 2 +- man/st_min_cuts.Rd | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/man/biconnected.components.Rd b/man/biconnected.components.Rd index 43d62a46e92..56f6906c45b 100644 --- a/man/biconnected.components.Rd +++ b/man/biconnected.components.Rd @@ -17,7 +17,7 @@ it is directed.} consistent API. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_biconnected_components}{\code{biconnected_components()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_biconnected_components}{\code{biconnected_components()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \keyword{internal} diff --git a/man/biconnected_components.Rd b/man/biconnected_components.Rd index edab1796bfc..3ee2ebdfc8d 100644 --- a/man/biconnected_components.Rd +++ b/man/biconnected_components.Rd @@ -46,7 +46,7 @@ that this is not true for vertices: the same vertex can be part of many biconnected components. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_biconnected_components}{\code{biconnected_components()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_biconnected_components}{\code{biconnected_components()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \examples{ diff --git a/man/graphlet_basis.Rd b/man/graphlet_basis.Rd index 045b239d4b0..9c5fe9dda58 100644 --- a/man/graphlet_basis.Rd +++ b/man/graphlet_basis.Rd @@ -76,7 +76,7 @@ the algorithm, and they are useful if the user wishes to perform them individually: \code{graphlet_basis()} and \code{graphlet_proj()}. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_candidate_basis}{\code{graphlets_candidate_basis()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_project}{\code{graphlets_project()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets}{\code{graphlets()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_candidate_basis}{\code{graphlets_candidate_basis()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_project}{\code{graphlets_project()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets}{\code{graphlets()}} } \examples{ diff --git a/man/graphlets.candidate.basis.Rd b/man/graphlets.candidate.basis.Rd index b72de6dc592..7c2f37be8d0 100644 --- a/man/graphlets.candidate.basis.Rd +++ b/man/graphlets.candidate.basis.Rd @@ -21,7 +21,7 @@ attribute is used.} consistent API. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_candidate_basis}{\code{graphlets_candidate_basis()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Graphlets.html#igraph_graphlets_candidate_basis}{\code{graphlets_candidate_basis()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \keyword{internal} diff --git a/man/k_shortest_paths.Rd b/man/k_shortest_paths.Rd index 1dfe09896ef..dbeaaa71f66 100644 --- a/man/k_shortest_paths.Rd +++ b/man/k_shortest_paths.Rd @@ -56,7 +56,7 @@ vertex in order of increasing length. Currently this function uses Yen's algorithm. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_get_k_shortest_paths}{\code{get_k_shortest_paths()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Structural.html#igraph_get_k_shortest_paths}{\code{get_k_shortest_paths()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \references{ diff --git a/man/simple_cycles.Rd b/man/simple_cycles.Rd index 2d769957c35..fe6deab4f1a 100644 --- a/man/simple_cycles.Rd +++ b/man/simple_cycles.Rd @@ -59,7 +59,7 @@ have exponentially many cycles and the presence of multi-edges exacerbates this combinatorial explosion. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Cycles.html#igraph_simple_cycles}{\code{simple_cycles()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Cycles.html#igraph_simple_cycles}{\code{simple_cycles()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \examples{ diff --git a/man/stCuts.Rd b/man/stCuts.Rd index 437d55c739f..8460ca93d26 100644 --- a/man/stCuts.Rd +++ b/man/stCuts.Rd @@ -20,7 +20,7 @@ stCuts(graph, source, target) consistent API. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_cuts}{\code{all_st_cuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_cuts}{\code{all_st_cuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \keyword{internal} diff --git a/man/stMincuts.Rd b/man/stMincuts.Rd index 855722247d1..993071048de 100644 --- a/man/stMincuts.Rd +++ b/man/stMincuts.Rd @@ -26,7 +26,7 @@ here.} consistent API. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_mincuts}{\code{all_st_mincuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_mincuts}{\code{all_st_mincuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \keyword{internal} diff --git a/man/st_cuts.Rd b/man/st_cuts.Rd index 1a19298155f..b4f5ac16afd 100644 --- a/man/st_cuts.Rd +++ b/man/st_cuts.Rd @@ -38,7 +38,7 @@ removing these edges from \eqn{G} there is no directed path from \eqn{s} to \eqn{t}. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_cuts}{\code{all_st_cuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_cuts}{\code{all_st_cuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \examples{ diff --git a/man/st_min_cuts.Rd b/man/st_min_cuts.Rd index 073ea57f62f..1a550a0a5b3 100644 --- a/man/st_min_cuts.Rd +++ b/man/st_min_cuts.Rd @@ -54,7 +54,7 @@ simply the number of edges. An \eqn{(s,t)}-cut is minimum if it is of the smallest possible size. } \section{Related documentation in the C library}{ -\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_mincuts}{\code{all_st_mincuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} +\href{https://igraph.org/c/html/0.10.17/igraph-Flows.html#igraph_all_st_mincuts}{\code{all_st_mincuts()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_edges}{\code{edges()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_get_eids}{\code{get_eids()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_ecount}{\code{ecount()}} } \examples{ From 79fadf6d0057e5a1e239a74e0114ed2b6e6d315b Mon Sep 17 00:00:00 2001 From: David Schoch Date: Fri, 5 Jun 2026 21:45:15 +0200 Subject: [PATCH 9/9] fix: avoid non-API DATAPTR in lazy-names ALTREP R CMD check on R 4.5 flagged a non-API call to `DATAPTR`, used by the `igraph_lazy_names` ALTREP Dataptr method. Switch to `DATAPTR_RO` (and keep `DATAPTR_OR_NULL`), both of which are part of the API and are the sanctioned replacements; `DATAPTR` is on tools:::nonAPI, these are not. The materialized name cache is read-only for our purposes (as.vector(), coercion, printing), so a read-only data pointer is sufficient. Verified the compiled object no longer references the `DATAPTR` entry point and that names/as_ids/as.vector/subsetting/printing still work. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/rinterface_extra.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/rinterface_extra.c b/src/rinterface_extra.c index 2965c7e52d2..4f25e94c697 100644 --- a/src/rinterface_extra.c +++ b/src/rinterface_extra.c @@ -2547,8 +2547,12 @@ static SEXP Rx_igraph_lazy_names_materialize(SEXP vec) { return data; } +/* DATAPTR_RO / DATAPTR_OR_NULL are used here rather than DATAPTR: the latter + * is non-API as of R 4.5. The materialized cache is a standard read-only name + * vector, so a read-only data pointer is all callers (as.vector(), coercion) + * need. */ static void *Rx_igraph_lazy_names_dataptr(SEXP vec, Rboolean writeable) { - return DATAPTR(Rx_igraph_lazy_names_materialize(vec)); + return (void *) DATAPTR_RO(Rx_igraph_lazy_names_materialize(vec)); } static const void *Rx_igraph_lazy_names_dataptr_or_null(SEXP vec) {