Skip to content

[LinearAlgebra] CompressedRowSparseMatrix family: Remove unused/dead code - #6271

Open
fredroy wants to merge 12 commits into
sofa-framework:masterfrom
fredroy:crs_remove_deadcode
Open

[LinearAlgebra] CompressedRowSparseMatrix family: Remove unused/dead code#6271
fredroy wants to merge 12 commits into
sofa-framework:masterfrom
fredroy:crs_remove_deadcode

Conversation

@fredroy

@fredroy fredroy commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Based on

Removing dead code/unreachable functions, or even in-compilable code (but never reached)


d3aac90 check_matrix() — both overloads, no callers, non-functional
289ebfe touchedBlock — declared, cleared once, never read or written
073c5f8 readVector() / writeVector() — protected, never called
d90377f RowConstIterator::operator* — uncallable, Real not deducible

diff : fredroy/sofa@fix_crs_problematic_unittests...crs_remove_deadcode

I guess it is making this PR breaking...


By submitting this pull request, I acknowledge that
I have read, understand, and agree SOFA Developer Certificate of Origin (DCO).


Reviewers will merge this pull-request only if

  • it builds with SUCCESS for all platforms on the CI.
  • it does not generate new warnings.
  • it does not generate new unit test failures.
  • it does not generate new scene test failures.
  • it does not break API compatibility.
  • it is more than 1 week old (or has fast-merge label).

fredroy and others added 12 commits August 28, 2026 12:37
block() short-circuits on "is j the first or the last column of this row?"
without first checking that the row range is non-empty. A registered row may
hold no block at all -- fullRows() and fullDiagonal() both create such rows --
and then colsIndex[rowRange.first] reads the next row's first entry while
colsIndex[rowRange.second - 1] reads the previous row's last one, which is
index -1 for the first row.

Reading any empty row therefore returned another row's value, or read out of
bounds. On a 6x6 matrix holding only (4,4) = 7, block(0,4) returned 7 and
block(0,0) returned uninitialised memory.

The same row lookup and column lookup are open-coded at a dozen sites, so
extract them into findRow() and findColInRange() and use them here. Both
helpers fold in the guards the copies were missing: the empty-range check, and
the nBlockRow == 0 / nBlockCol == 0 division guard.

Fixes CompressedRowSparseMatrixGeneric.BlockOnEmptyRowAfterFullRows,
CompressedRowSparseMatrixGeneric.BlockOnEmptyRowDoesNotBorrowNeighbourValue and
CompressedRowSparseMatrixMechanical.ElementAfterFullRows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 2b234b6ec19dc962d0a24590c59e390869c2c27e)
clearRowBlock() called rowIndex.back() and rowIndex.front() as fast paths
before checking that rowIndex was non-empty, so clearing a row of a matrix with
no registered row dereferenced past a null pointer and segfaulted.

findRow() already reports an empty matrix as "row not found", so use it here
instead of the open-coded lookup.

Fixes CompressedRowSparseMatrixGenericDeathTest.ClearRowBlockOnEmptyMatrix,
CompressedRowSparseMatrixGenericDeathTest.ClearRowBlockOnDefaultConstructedMatrix
and CompressedRowSparseMatrixConstraintDeathTest.ClearRowBlockOnEmptyMatrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 14d101ebcb2fa13c352e2ec298e54c8137c0b40b)
clearRowColBlock() computed foundRowId but never acted on it. When the row
lookup failed, rowId kept whatever value the failed binary search left behind,
and that value was then used to build rowRange -- reading out of bounds -- and
passed straight to deleteRow(). On a matrix where row i is absent but column i
exists, this dropped a completely unrelated row: clearing row/column 0 of a
matrix holding (1,0), (1,1) and (2,2) deleted row 1, losing (1,1).

An absent row is normal in a sparse matrix and column i may still hold blocks
in other rows, so it is not an error either. Reserve the diagnostic for an
index that is genuinely outside the matrix, delete the row only when findRow()
locates it, and always clear the column.

clearColBlock() carried its own copy of the unguarded column lookup, so route
it through findColInRange() as well since clearRowColBlock() delegates to it.

Fixes CompressedRowSparseMatrixGeneric.ClearRowColBlockWithAbsentRow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 3275851fb0e1c693bb1f4828221867d26aa44ece)
Two defects in the symmetric half of clearRowCol().

First, having read the block column j out of colsIndex, it built the symmetric
block's range as rowBegin[j] .. rowBegin[j + 1]. rowBegin is indexed by the
position of a row inside rowIndex, not by the row number; the two coincide only
when every row is present. On a sparse matrix this searched the wrong row --
reading out of bounds when j exceeded the number of registered rows -- and so
failed to clear entries that are in column i. Clearing row/column 2 of an 8x8
matrix holding rows 2..5 searched row 5's range for the symmetric block and
left (3,2) untouched.

Second, when the symmetric block (j,i) was not found, the local pointer was
never reset, so the second loop zeroed a column of block (i,j) instead --
entries that lie in neither row i nor column i. Only observable for NL > 1;
with 3x3 blocks, clearing row/column 0 wiped (1,3) and (2,3).

Look row j up with findRow() before taking its range, locate the symmetric
block with findColInRange(), and skip the second clear entirely when it does
not exist. Handle the diagonal block explicitly, since it is its own symmetric
counterpart. This also drops an unguarded division by nBlockRow.

Fixes CompressedRowSparseMatrixMechanical.ClearRowColMissesSymmetricBlockWhenRowsAreSparse
and CompressedRowSparseMatrixMechanical.ClearRowColAsymmetricPatternMat3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 230d96cf49e3d0d895468d481e4f728cd20ab517)
filterValues() only pushed a destination row when that row had produced at
least one value, so the keepEmptyRows flag threaded through all eight copy*()
wrappers did nothing. The compensating pop_back() meant to undo the push could
never fire either: rows are only pushed once oldVid != vid, so rowBegin.back()
is always strictly less than vid at that point.

Push the row when the caller asked to keep empty ones, and drop the dead
pop_back().

The flag concerns destination rows emptied by the filter itself, not source
rows that were never registered: filterValues() only iterates
srcMatrix.rowIndex. No caller in the tree passes keepEmptyRows; every call uses
the default false.

Fixes CompressedRowSparseMatrixMechanical.CopyNonZerosKeepEmptyRows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RowConstIterator's defaulted default constructor left m_internal and m_matrix
without initialisers. A default-constructed iterator therefore held an
indeterminate row position and a dangling matrix pointer, and its own
isInvalid() reported false -- so it read as a valid iterator onto row 0 of an
unspecified matrix.

Give both members a default member initialiser so the default-constructed state
is the invalid one the class already knows how to describe.

Fixes CompressedRowSparseMatrixConstraint.DefaultConstructedRowIteratorIsInvalid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…arded helpers

The "find row i" and "find column j inside a row" idioms were open-coded at a
dozen more sites. Each copy carried the same two hazards the previous commits
fixed at individual call sites: an unguarded division by nBlockRow / nBlockCol,
undefined on a default-constructed matrix, and the first/last-column fast paths
dereferencing colsIndex without checking the row range for emptiness.

The division silently yields 0 on ARM, which is why the accompanying guard
tests pass there, but integer division by zero raises SIGFPE on x86_64.
Confirmed with UBSan at Mechanical.h:343 (clearRow), :748 (blockGet) and :841
(bRowBegin) before this change, clean after.

Converted: clearRow, blockGet, blockGetW, blockCreate, bRowBegin, bRowEnd and
bRowRange in the mechanical matrix; both wblock() overloads -- including the
hinted one, whose divisions had no guard at all -- and getMaxColIndex in the
generic one. The ordered-insertion append path now treats an empty range on the
last row as "append here" rather than reading colsIndex[-1].

No behaviour change on well-formed input; this is the undefined behaviour left
over once the six defects with failing tests were fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check_matrix() has no callers anywhere in the tree, and would not work if it
had any:

- the non-static overload takes &rowBegin[0], &colsIndex[0] and &colsValue[0]
  without checking the vectors are non-empty;
- it walks a_p from 1 to m, where m is rowBSize(), but rowBegin holds
  rowIndex.size() + 1 entries -- the two only match after fullRows();
- it requires a_p to increase strictly, so any empty row is reported as an
  error;
- the column loop increments i both in the for-header and in the body, so most
  entries are never checked;
- one diagnostic prints a_p[i] where it means a_i[i];
- and success is reported through msg_error.

Delete it rather than repair a validator nothing calls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
touchedBlock was declared alongside the CSR arrays and cleared once in
compressBtemp(), but never written to and never read -- by this class or by
anything else in the tree. Its documented purpose, tracking which blocks were
touched since the last compression, was never implemented.

It was also the one data member swap() did not exchange, so swapping two
matrices left it behind; removing it makes that discrepancy moot.

The VecFlag alias it used is kept: it is now unused in-tree, but it is public
API on both CRSBlockTraits and the matrix classes, so out-of-tree code may
still name it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither helper has ever been called: write()/read() serialise the CSR arrays
through the stream operators instead, and nothing outside the class can reach
these two since they are protected.

readVector() would also have been wrong for most instantiations. It parses
every entry through safeStrToInt into an int before pushing it back, so it
cannot round-trip a vector of blocks or of any real type -- only the index
vectors.

Dropping them leaves sofa/type/hardening.h unused in this header, so remove
that include too. Verified that no transitive consumer depended on it by
rebuilding Sofa.Core, Sofa.Component.Constraint.Projective,
Sofa.Component.LinearSolver.Direct, Sofa.Component.LinearSystem,
Sofa.Component.StateContainer, Sofa.Component.SolidMechanics.FEM.Elastic and
Sofa.Component.Mapping.Linear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RowConstIterator declared

    template <class VecDeriv, typename Real>
    Real operator*(const VecDeriv& v) const

but Real appears only in the return type, so it can be neither deduced from the
argument nor defaulted, and operator syntax offers no way to supply it
explicitly. No call to it can compile: overload resolution does not even
consider the member, and a *rowIt * v expression fails with "indirection
requires pointer operand".

The functionality it duplicated is available, and actually used, through the
free CompressedRowSparseMatrixVecDerivMult(row, vec), which defaults Real to
VecDeriv::value_type::Real.

Removed rather than repaired: reinstating it would mean giving Real that same
default, which turns a member nothing has ever been able to call into new API.
That is a deliberate addition, not dead-code cleanup, so it is left out of this
series.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fredroy fredroy added pr: breaking Change possibly inducing a compilation error pr: status to review To notify reviewers to review this pull-request pr: clean Cleaning the code pr: AI-aided Label notifying the reviewers that part or all of the PR has been generated with the help of an AI labels Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: AI-aided Label notifying the reviewers that part or all of the PR has been generated with the help of an AI pr: breaking Change possibly inducing a compilation error pr: clean Cleaning the code pr: status to review To notify reviewers to review this pull-request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant