Skip to content

The scratch-pool lock is the guard's API rather than a convention, because one caller had skipped it - #1246

Merged
Rafael-SOWNet merged 1 commit into
masterfrom
determinant-lock
Sep 10, 2026
Merged

Rafael-SOWNet merged 1 commit into
masterfrom
determinant-lock

Conversation

@Rafael-SOWNet

Copy link
Copy Markdown
Member

FractionFreeDeterminantTest.EliminationAgreesWithLaplaceWhereverBothApply fails on master
whenever it happens to run beside MatrixConcurrencyTest, and it fails in the way #1219 was
filed for — the determinant of a matrix whose entries are integers and a, b, c comes back
containing sin(a), which is an entry of the other test's matrices:

[[-4, 3, -1], [-2, a, 0], [1, a, 3]]:
  -9 * a + 18   vs   (-4) * (a * 3 + -sin(a) * 2 * sin(a)) + 18 + -((-2) * a + -a)

Thirteen of the three hundred generated matrices disagreed. Reproducing it needs nothing but the
two classes together, since xUnit runs test classes in parallel:

dotnet test Sources/Tests/UnitTests/UnitTests.csproj -c Release \
  --filter "FullyQualifiedName~MatrixConcurrencyTest|FullyQualifiedName~FractionFreeDeterminantTest"

Failed: 1 on master. Each class on its own passes, which is why this has not been noticed.

Why the #1230 guard did not cover it

It covers all three of the library's callers. This one is in the test tree: it reaches
DeterminantLaplace directly, because comparing Laplace against the elimination is the whole
point of the test and Determinant would answer by elimination. One unguarded caller is enough
to break every guarded one — it writes into the shared scratch matrix while they are reading it,
and reads their minors back.

What this changes

Locking in the test would fix this instance. Instead the lock object becomes private and the
three pool operations — DeterminantLaplace, Adjoint, InvertMatrix — become the guard's whole
surface, so a caller can no longer reach GenericTensor without the lock. The library's three call
sites lose their lock statements and gain a method call; the test calls the same method.

Adjoint returns the Entity rather than the tensor deliberately. The lock has to cover reading
the elements out of the tensor, and Entity.Matrix's constructor is where they are read, so a
GenTensor-returning signature would put that read on the far side of the lock. That is the
bracket the previous code had, kept.

No public API moves, and no answer changes on a single thread.

Measured

Three runs of the failing command with this change: Failed: 0 each time.

suite
UnitTests (!~Calculus) 8514 passed
UnitTests (~Calculus) 1508 passed
FSharpWrapperUnitTests 134 passed
InteractiveWrapperUnitTests 18 passed
TerminalUnitTests 41 passed

AotSmokeTest was left to CI — this change adds no reflection or generic instantiation, and
CPPWrapperUnitTests is a .bat.

Part of #1219, which was closed by #1230 and turns out to have had one caller left.

🤖 Generated with Claude Code

https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura

…cause one caller had skipped it

FractionFreeDeterminantTest.EliminationAgreesWithLaplaceWhereverBothApply
fails on master whenever it runs beside MatrixConcurrencyTest, and it fails by
reporting the determinant of a matrix of integers and a, b, c as containing
sin(a) -- entries that belong to the other test's matrices.

The guard added in #1230 covers GenericTensor's process-wide scratch matrix at
all three of the library's call sites. It did not cover this one, which reaches
DeterminantLaplace directly in order to compare Laplace against the elimination,
and one unguarded caller is enough: it corrupts every guarded caller and reads
their minors back.

Taking the lock in the test would fix this instance. Making the lock private and
the three pool operations -- DeterminantLaplace, Adjoint, InvertMatrix -- the
guard's whole surface fixes the shape, since a caller can then no longer reach
GenericTensor without it. That is what this does.

Adjoint returns the entity rather than the tensor for the same reason. The lock
has to cover reading the elements out, and Entity.Matrix's constructor is where
they are actually read, so a GenTensor-returning signature would put that read
on the far side of the lock.

Reproduction, on master, this machine:

    dotnet test --filter "FullyQualifiedName~MatrixConcurrencyTest|FullyQualifiedName~FractionFreeDeterminantTest"
    Failed: 1  (13 of 300 determinants disagree)

and with this change, three runs of the same command, Failed: 0 each time.

Suites: UnitTests 8514 and 1508, FSharpWrapperUnitTests 134,
InteractiveWrapperUnitTests 18, TerminalUnitTests 41.

#1219

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura
@Rafael-SOWNet
Rafael-SOWNet merged commit 5ef6dcd into master Sep 10, 2026
31 checks passed
@Rafael-SOWNet
Rafael-SOWNet deleted the determinant-lock branch September 10, 2026 10:09
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.

1 participant