From ba87812158b66353cc0c01dfed2772e5ed442033 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 7 Aug 2026 23:28:57 +0000 Subject: [PATCH] Pin the temporary-Entity lifetime #367 reports #367 says a method called on a temporary Entity throws NonExistentObjectAddressingException, the temporary having been destroyed and its handle released before the call using it returned: auto simplified = AngouriMath::Entity("x + 2").Simplify(); Measured against master, it does not reproduce -- that line answers `2 + x`. `work/TRIAGE.md` had this recorded as unreachable, on the grounds that there is no C++ toolchain set up here. That was wrong, and is the fifth recorded blocker in this file to fall on being re-measured rather than read. There is g++ 15.2 and make; only cmake is missing, and the native AOT export publishes fine on linux-x64, so the wrapper can be compiled and run directly against the .so without it. Five tests rather than one, because a lifetime bug that is gone should be gone in every shape it would have shown in: the issue's own line, a result outliving the temporary it came from, two calls chained off one temporary, a result copied out of the scope that made it, and two hundred repetitions to shake loose a handle that is freed but not yet reused. All 28 tests in the file compile and pass against the native library built from this commit. Co-Authored-By: Claude Opus 5 (1M context) --- .../CPPWrapperUnitTests/tests/RunTests.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Sources/Tests/CPPWrapperUnitTests/tests/RunTests.cpp b/Sources/Tests/CPPWrapperUnitTests/tests/RunTests.cpp index 704711929..ad0300592 100644 --- a/Sources/Tests/CPPWrapperUnitTests/tests/RunTests.cpp +++ b/Sources/Tests/CPPWrapperUnitTests/tests/RunTests.cpp @@ -1,5 +1,6 @@ #include #include +#include TEST(RunTests, ParsingTest1) { auto src = "x / 2 + 3"; @@ -174,3 +175,39 @@ TEST(RunTests, ToComplex) { EXPECT_EQ(6.0, com.real()); EXPECT_EQ(1.0, com.imag()); } + +// A method called on a temporary Entity threw NonExistentObjectAddressingException: the +// temporary was destroyed, and its handle released, before the call that used it returned. +// https://github.com/asc-community/AngouriMath/issues/367 +// +// It does not reproduce on this version. The issue's own line is first, then the shapes a +// too-early release would show up in and this one did not: a result outliving the temporary +// it came from, two calls chained off one, a temporary passed as an argument, a result +// copied out of the scope that made it, and enough repetitions to shake loose a handle that +// is freed but not yet reused. +TEST(RunTests, TemporaryEntityOutlivesTheCallOnIt) { + EXPECT_EQ(AngouriMath::Entity("x + 2").Simplify().ToString(), "2 + x"); +} + +TEST(RunTests, ResultOutlivesTheTemporaryItCameFrom) { + auto simplified = AngouriMath::Entity("x + 2").Simplify(); + EXPECT_EQ(simplified.ToString(), "2 + x"); +} + +TEST(RunTests, TwoCallsChainedOffOneTemporary) { + EXPECT_EQ(AngouriMath::Entity("(x + 2) * 1").Simplify().Differentiate("x").ToString(), "1"); +} + +TEST(RunTests, ResultCopiedOutOfTheScopeThatMadeIt) { + std::vector kept; + { + kept.push_back(AngouriMath::Entity("sin(x) ^ 2 + cos(x) ^ 2").Simplify()); + } + EXPECT_EQ(kept[0].ToString(), "1"); +} + +TEST(RunTests, ManyTemporariesDoNotReleaseAHandleInUse) { + for (int i = 0; i < 200; i++) { + EXPECT_EQ(AngouriMath::Entity("x + 2").Simplify().ToString(), "2 + x"); + } +}