Skip to content

fix: Calling igraph from a warning handler no longer corrupts the call that warned - #2999

Merged
krlmlr merged 2 commits into
claude/k1-categorize-up-frontfrom
claude/v2-warning-reentrancy
Oct 18, 2026
Merged

krlmlr merged 2 commits into
claude/k1-categorize-up-frontfrom
claude/v2-warning-reentrancy

Conversation

@krlmlr

@krlmlr krlmlr commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

A warning from the C core can now be handled by R code that calls igraph again, without corrupting the call that warned. It is stacked on #2998.

Closes #2983.

The problem

A C warning is signalled from IGRAPH_R_CHECK() while the generated wrapper still holds its output vectors on the igraph finally stack, and keeps the attributes of a graph it is about to return alive only through the attribute preserve list. A calling handler that called igraph again ran R_igraph_finalizer(), whose IGRAPH_FINALLY_FREE() destroyed the outer vectors, and cleared the preserve list. The outer call then failed with "Assertion failed: v->stor_begin != NULL" or "Corrupt finally stack", or returned a graph whose attributes could be garbage-collected.

What it changes

Rx_igraph_warning() in src/rinterface_extra.c now signals the warning in a new finally stack level, with the preserve list and a pending error saved, and restores them through R_UnwindProtect(), also when a handler jumps out (tryCatch(), options(warn = 2)). Nested calls only ever free their own entries. Nothing changes at the hundreds of .Call() sites.

The second commit keeps the call of the warning. Inside R_UnwindProtect(), Rf_warning() took its call from the context that R_UnwindProtect() opens: a byte-compiled caller still got its call through the bytecode interpreter, but under pkgload::load_all(), covr, or with the JIT disabled, a C warning lost "In triad_census_impl()", and six snapshots in test-aaa-auto.R failed. The warning is now signalled through base::warning(), which names the closure below it, as Rf_warning() did before. Warnings keep their text, their "Source: file:line" suffix, their call and their timing: the output under options(warn = 1) and options(warn = 2) is identical to main's.

tests/testthat/test-warning-reentrancy.R adds six tests: a handler that calls igraph, a logging handler that looks at the graph that warned, a returned graph that keeps its attributes when the handler collects garbage, igraph staying usable after a handler jumps out, and C warnings keeping their text, source, count and call.

Checks

With a handler that calls degree(make_ring(3)) and muffles the warning, a build of the base of this PR fails with the assertion for triad_census(), for eigen_centrality() and hits_scores() with a negative weight, and with "Corrupt finally stack" for connect(); a build of this branch returns all four normally. hits_scores(make_ring(4)) does not warn with the current C core, so it only triggers once the C core 1.0 warns for undirected graphs. Three of the first five new tests error on the base build. The whole suite passes against an installed build of this branch (1912 blocks), except graph_from_graphdb(), which could not download its data in the sandbox. devtools::document() and air format change nothing.

R callbacks that run during a C call, such as a bfs() callback that calls degree(), share the finally stack problem; they are #2548 and are not addressed here.

LLM use: this PR was prepared with Claude Code (Claude Opus 5.5), at the maintainer's request. The fix comes from the subsession https://claude.ai/code/session_017Ajt8pqcYoxFsEW8KhcTSY; the second commit was added in this session.

  • By submitting this pull request, I assign the copyright of my contribution to The igraph development team.

Generated by Claude Code

A warning from the C core is signalled from `IGRAPH_R_CHECK()`
while the generated wrapper still holds its output vectors on the finally stack
and keeps the attributes of a result graph alive through the preserve list.
A calling handler that calls igraph again ran `R_igraph_finalizer()`,
whose `IGRAPH_FINALLY_FREE()` destroyed the outer vectors,
and cleared the preserve list,
so the outer call failed with "Assertion failed: v->stor_begin != NULL"
or "Corrupt finally stack".

`Rx_igraph_warning()` now signals the warning in a new finally stack level,
with the preserve list and a pending error saved,
and restores them through `R_UnwindProtect()`,
also when a handler jumps out.
Warnings keep their text, their source suffix and their timing.

Fixes #2983.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ajt8pqcYoxFsEW8KhcTSY
Signalled inside `R_UnwindProtect()`, `Rf_warning()` took its call from the context that `R_UnwindProtect()` opens.
A byte-compiled caller still got its call through the bytecode interpreter,
but under `pkgload::load_all()`, covr, or with the JIT disabled,
a C warning lost "In `triad_census_impl()`", and six snapshots in `test-aaa-auto.R` failed.

`Rx_igraph_warning_signal()` now signals through `base::warning()`,
which names the closure below it, as `Rf_warning()` did before the frame existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnA76zWTu4ZD8BQCSYRpk
@krlmlr
krlmlr added this pull request to stack #3001 October 18, 2026 13:00
@krlmlr
krlmlr merged commit 2ab9860 into main Oct 18, 2026
9 checks passed
@krlmlr
krlmlr deleted the claude/v2-warning-reentrancy branch October 18, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling igraph from a warning handler corrupts the warning call ("Assertion failed: v->stor_begin != NULL")

2 participants