java-topology/defects/r-source/patch/r-source-0002-walkClassGraph-match-dedup.md
russell@unturf.com 25c2bafdee undf: assign 694-720; stamp patches; ruby-0003/elixir-0002/r-source-0002/victoria-metrics-0002
New UNDF assignments (693→720):
  elixir-0002 → UNDF-2026-000000698 (typespec used_type_pairs O(T²))
  r-source-0002 → UNDF-2026-000000711 (.walkClassGraph match dedup O(S²))
  ruby-0003 → UNDF-2026-000000712 (RubyGems dependent_gems O(N²×D))
  victoria-metrics-0002 → UNDF-2026-000000717 (MetricName tag-filter O(T×I))

Total: 720 UNDF assigned
2026-03-29 22:28:31 -04:00

111 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000711
# r-source-0002: .walkClassGraph — O(S²) match() dedup during S4 class registration
## Severity: MEDIUM
## Location
- `src/library/methods/R/RClassUtils.R` — function `.walkClassGraph`
- Line ~1126: `exti <- exti[is.na(match(names(exti), what))]`
## Description
`.walkClassGraph` computes the transitive closure of super/subclass relationships
when a new S4 class is defined via `setClass()` or `setIs()`. For a class with S
known superclasses, the function loops over each known relation and merges in
the transitive superclasses of each intermediate class:
```R
# RClassUtils.R ~ line 1112-1140
.walkClassGraph <- function(ClassDef, slotName, where, conflicts = character()) {
ext <- slot(ClassDef, slotName) # initial super/subclasses
what <- names(ext) # names accumulated so far
for (i in seq_along(ext)) { # O(S) iterations over original ext
by <- what[[i]]
byDef <- getClassDef(by, ...)
exti <- slot(byDef, slotName) # indirect classes via this intermediate
## Remove already-known relations:
exti <- exti[is.na(match(names(exti), what))] # O(|exti| × |what|) !
if (length(exti)) {
ext <- c(ext, exti) # what grows as we add new classes
# ... further processing
}
}
# ...
}
```
The problem: `match(names(exti), what)` is O(|exti| × |what|) — a linear scan of
`what` for each element of `names(exti)`. Both `exti` and `what` can grow to O(S)
where S = number of transitive superclasses. Since this runs inside a loop of O(S)
iterations, total cost is O(S³) in the worst case, or O(S²) for typical class
hierarchies where each intermediate adds a constant number of new superclasses.
Called from: `completeSubclasses()``setIs()``setClass()` for every superclass
in the `contains=` argument.
## Root Cause
`what` is a character vector. `match(x, what)` does a linear scan. No hash set is
maintained alongside `what` to support O(1) deduplication.
## Fix
Maintain a `character` set (or simulated via a named list/environment) for O(1)
membership testing:
```diff
--- a/src/library/methods/R/RClassUtils.R
+++ b/src/library/methods/R/RClassUtils.R
@@ .walkClassGraph
.walkClassGraph <- function(ClassDef, slotName, where, conflicts = character()) {
ext <- slot(ClassDef, slotName)
+ # Build a fast-lookup environment: name -> TRUE (O(1) membership)
+ what_set <- new.env(hash = TRUE, parent = emptyenv(), size = length(ext) * 2L)
what <- names(ext)
+ for (nm in what) assign(nm, TRUE, envir = what_set)
for (i in seq_along(ext)) {
by <- what[[i]]
if (isClass(by, where = packageSlot(ext[[i]]))) {
byDef <- getClassDef(by, package = packageSlot(ext[[i]]))
exti <- slot(byDef, slotName)
# ...
- ## O(|exti| × |what|) linear scan:
- exti <- exti[is.na(match(names(exti), what))]
+ ## O(|exti|) hash lookup:
+ exti <- exti[!vapply(names(exti), exists, logical(1L), envir = what_set)]
if (length(exti)) {
# ...
ext <- c(ext, exti)
+ for (nm in names(exti)) assign(nm, TRUE, envir = what_set)
what <- names(ext)
}
}
}
# ...
}
```
## Complexity
S = number of transitive superclasses
| S | Before (match ops) | After (hash ops) | Ratio |
|------|--------------------|------------------|-------|
| 10 | ~100 | ~10 | 10× |
| 30 | ~900 | ~30 | 30× |
| 100 | ~10,000 | ~100 | 100× |
## Impact
Standard R/CRAN packages: S typically 515 (low impact).
Bioconductor S4 packages (e.g., `BiocGenerics`, `SummarizedExperiment`, `SingleCellExperiment`):
S can reach 3060 transitive superclasses per class. Package loading triggers `setClass()`
for every exported class — with 50 classes each having S=40 superclasses, the fix
reduces class-load time by ~40×.
The `anyDuplicated(what)` call later in `.walkClassGraph` (line ~1173) also becomes
unnecessary if we maintain the hash set, since duplicates are prevented at insertion.