Skip to content

Keep exception and string display from populating string caches - #648

Merged
lagergren merged 3 commits into
masterfrom
lagergren/569-7-exceptionhandle
Sep 26, 2026
Merged

lagergren merged 3 commits into
masterfrom
lagergren/569-7-exceptionhandle

Conversation

@lagergren

@lagergren lagergren commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Rendering a StringHandle in a Java debugger populated its Java String cache. ExceptionHandle display did the same to its text handle, including when Java printed the wrapper exception's stack trace. This PR keeps the rendered text unchanged while leaving those caches alone.

Reproducer and debugger impact

A debugger that evaluates toString() can change the state being inspected:

  1. Create a non-empty handle with xString.makeHandle(...). Its m_sValue cache is null.
  2. Evaluate handle.toString(), as an automatic debugger renderer does.
  3. Before this change, m_sValue now contains the string even though application code has not called getStringValue(). With this change, it remains null.

That can confuse investigation of when the cache is first populated: inspecting the handle itself warms the cache. ExceptionHandle.toString(), its WrapperException.toString(), and Java stack-trace printing have the corresponding text-cache path.

The reproduced impact is memoization during inspection. This PR does not claim a changed application result, a hang, or an uninitialized-layout crash. Display still allocates ordinary Java strings as needed.

Changes

  • StringHandle.toString() reads an existing cached value, or creates the display string from its characters without storing it.
  • ExceptionHandle.toString() keeps the ordinary getField(null, "text") read and quotes the string's characters without calling the memoizing getStringValue().
  • HandleDisplayTest exercises the real display methods with actual handles from a NativeContainer and the installed XDK. It checks escaped output, initially empty caches, already populated caches, wrapper rendering, and Java stack-trace printing.

Field-access review

For a valid constructed ExceptionHandle, text is an existing ordinary field. Its composition
layout is prepared before construction, and GenericHandle initializes the corresponding field
array. getField(null, "text") therefore reads the stored value; it does not populate the
StringHandle's cache. Even an unassigned field value can be handled by the existing instanceof
check.

The unwanted mutation was in the subsequent getStringValue() call. There was no demonstrated
failure of this normal field read to justify a separate accessor. Both the speculative layout
guard and the peekField() helper are removed. GenericHandle, ClassComposition and
TypeComposition now have no net changes in this PR.

Verification

Final diff: 3 files, +110 / −6. The field-access simplification is
611fb1e9e.

After restoring the ordinary field read, the existing regression tests passed unchanged:
19 XDK cases, zero failures/errors/skips, including both display/cache cases. Formatting and
whitespace checks passed.

env RUN_INTEGRATION_TESTS=true ./gradlew :xdk:test --rerun --no-build-cache spotlessCheck --console=plain
git diff --check

The exception regression itself first reads getField(null, "text") and confirms that the
String cache is still empty. It then checks that handle display, wrapper display and Java
stack-trace printing leave the cache empty, and that an already populated cache is preserved.

The earlier before/after comparison temporarily restored the previous rendering methods.
Both cases failed at the cache assertion: expected null, but display populated the cache.
The output assertions preceding those checks passed. The temporary changes were restored.
That comparison tests the rendering change; it does not establish a need for an alternate
field accessor.

The tests use the distribution already provisioned by the XDK test task; no new build
dependency or silent skip was added.

Earlier broader verification at e650872 also passed: javatools 414 cases, 36 existing skips;
javatools_utils 120 cases, 2 existing skips; no failures or errors.

@lagergren
lagergren force-pushed the lagergren/569-7-exceptionhandle branch from f2b41e3 to ce01cc5 Compare September 26, 2026 08:37
@lagergren
lagergren requested a review from ggleyzer September 26, 2026 08:38
@lagergren
lagergren force-pushed the lagergren/569-7-exceptionhandle branch 2 times, most recently from 51687a0 to 7ebb554 Compare September 26, 2026 08:58
@lagergren lagergren changed the title Stop ExceptionHandle.toString() allocating a handle and memoizing a string Stop ExceptionHandle and StringHandle rendering from allocating and memoizing Sep 26, 2026
@lagergren
lagergren force-pushed the lagergren/569-7-exceptionhandle branch from 7ebb554 to 4003ca2 Compare September 26, 2026 09:05
Comment thread javatools/src/main/java/org/xvm/runtime/ObjectHandle.java Outdated
@lagergren lagergren changed the title Stop ExceptionHandle and StringHandle rendering from allocating and memoizing Keep exception and string display from populating string caches Sep 26, 2026
@lagergren
lagergren requested a review from ggleyzer September 26, 2026 14:46
Comment thread xdk/src/test/java/org/xvm/xdk/HandleDisplayTest.java
@lagergren
lagergren requested a review from ggleyzer September 26, 2026 15:04
@lagergren
lagergren force-pushed the lagergren/569-7-exceptionhandle branch from 611fb1e to 41b2cd6 Compare September 26, 2026 21:37
…emoizing

Two handle toString()s did more than display should, in the same way.

ExceptionHandle.toString() read its message with getField(null, "text") and rendered it with
StringHandle.getStringValue(). getField() reads the field layout, which is null until
ensureFieldLayout() has built it, and when the property is absent it calls
missingPropertyException(), allocating a DeferredCallHandle around a freshly constructed exception
handle - so rendering an exception could construct another one. This matters beyond a debugger,
because WrapperException.toString() delegates here and therefore runs on every stack-trace print.

peekField() returns the same handle in the case that renders: for a non-transient field of a built
layout both it and getField() end at m_aFields[field.getIndex()]. In every other case getField()
allocates, throws, or routes through a null frame, while peekField() returns null - and at the call
site null and a DeferredCallHandle are alike "not a StringHandle", so the rendered text is the same
either way. A transient "text" would previously have called getTransientField(null, field) with a
null frame, so that path was already broken rather than newly unsupported.

isFieldLayoutComputed() is the guard peekField() needs: false by default on TypeComposition, and the
real answer on ClassComposition, which owns the map.

StringHandle.toString() called getStringValue(), which memoizes:

    return sValue == null ? (m_sValue = new String(m_achValue)) : sValue;

The write is idempotent, but it is still a write performed by the act of observing. Both sites now
read the cache when it is present and build a throwaway String when it is not, which is the same
content getStringValue() would have produced and stored.

xString's builder goes with it: Handy.quotedString(s) is exactly the append('"')/appendString/
append('"') sequence it was doing by hand, so the method is now one expression and matches how
ExceptionHandle renders the same thing.

Verified by running two modules under master's javatools and this branch's - one throwing a caught
and an uncaught exception, one carrying a string with a quote, a backslash and a newline through
both a console print and an exception message. Both render identically, timestamp aside.

No unit test: both StringHandle and the handles peekField() reads require a TypeComposition to
construct, xString.makeHandle() goes through INSTANCE.getCanonicalClass(), and ObjectHandle.toString()
dereferences the composition - so a test needs either a live container or a fake spanning 25 abstract
methods. That is disproportionate here, and the equivalences above are by construction.
Exercise actual StringHandle, ExceptionHandle, wrapper rendering and Java stack traces using the installed XDK. Both regressions fail with the previous rendering methods because display populates the string cache; the corrected implementation preserves cold and warm cache state and escaped output.

Remove isFieldLayoutComputed: normal composition and handle construction already require the layout. Do not claim a reachable uninitialized-layout display failure.

Validation: 19 XDK cases passed without skips; javatools 414 cases with 36 existing skips and javatools_utils 120 with 2 existing skips, no failures or errors. Spotless and whitespace checks passed.
@lagergren
lagergren force-pushed the lagergren/569-7-exceptionhandle branch from 41b2cd6 to 3dae54e Compare September 26, 2026 21:55
@lagergren
lagergren enabled auto-merge (squash) September 26, 2026 22:04
@lagergren
lagergren merged commit 01479f2 into master Sep 26, 2026
4 checks passed
@lagergren
lagergren deleted the lagergren/569-7-exceptionhandle branch September 26, 2026 22:05
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.

2 participants