From ba06f632985dc998ed1e9f6e4ec6e1ef6d681861 Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Thu, 18 Sep 2025 16:58:59 -0400 Subject: [PATCH 1/8] Fix: BytecodeNode#setLocalValues should unwrap the frame from a continuation frame --- .../api/bytecode/test/LocalHelpersTest.java | 20 +++++++++++++------ .../truffle/api/bytecode/BytecodeNode.java | 14 ++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java index 3594e3979313..083f75cdd122 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java @@ -1002,9 +1002,9 @@ public void testGetLocalsContinuationStacktrace() { CallTarget collectFrames = new RootNode(null) { @Override public Object execute(VirtualFrame frame) { - List frames = new ArrayList<>(); + List frames = new ArrayList<>(); Truffle.getRuntime().iterateFrames(f -> { - frames.add(BytecodeNode.getLocalValues(f)); + frames.add(f); return null; }); return frames; @@ -1058,19 +1058,27 @@ public Object execute(VirtualFrame frame) { assertTrue(result instanceof List); @SuppressWarnings("unchecked") - List frames = (List) result; + List frames = (List) result; assertEquals(3, frames.size()); // - assertNull(frames.get(0)); + assertNull(BytecodeNode.getLocalValues(frames.get(0))); // bar - Object[] barLocals = frames.get(1); + Object[] barLocals = BytecodeNode.getLocalValues(frames.get(1)); assertArrayEquals(new Object[]{42}, barLocals); + Object[] barLocalNames = BytecodeNode.getLocalNames(frames.get(1)); + assertArrayEquals(new Object[]{"y"}, barLocalNames); + BytecodeNode.setLocalValues(frames.get(1), new Object[]{-42}); + assertArrayEquals(new Object[]{-42}, BytecodeNode.getLocalValues(frames.get(1))); // foo - Object[] fooLocals = frames.get(2); + Object[] fooLocals = BytecodeNode.getLocalValues(frames.get(2)); assertArrayEquals(new Object[]{123}, fooLocals); + Object[] fooLocalNames = BytecodeNode.getLocalNames(frames.get(2)); + assertArrayEquals(new Object[]{"x"}, fooLocalNames); + BytecodeNode.setLocalValues(frames.get(2), new Object[]{456}); + assertArrayEquals(new Object[]{456}, BytecodeNode.getLocalValues(frames.get(2))); } @Test diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java index 25a91851ccb3..ec204d8bc11d 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java @@ -1226,7 +1226,7 @@ public static Object[] getLocalValues(FrameInstance frameInstance) { if (bytecode == null) { return null; } - Frame frame = resolveFrame(frameInstance); + Frame frame = resolveFrame(frameInstance, FrameAccess.READ_ONLY); int bci = bytecode.findBytecodeIndexImpl(frame, frameInstance.getCallNode()); return bytecode.getLocalValues(bci, frame); } @@ -1265,16 +1265,14 @@ public static boolean setLocalValues(FrameInstance frameInstance, Object[] value return false; } int bci = bytecode.findBytecodeIndex(frameInstance); - bytecode.setLocalValues(bci, frameInstance.getFrame(FrameAccess.READ_WRITE), values); + bytecode.setLocalValues(bci, resolveFrame(frameInstance, FrameAccess.READ_WRITE), values); return true; } - private static Frame resolveFrame(FrameInstance frameInstance) { - Frame frame = frameInstance.getFrame(FrameAccess.READ_ONLY); - if (frameInstance.getCallTarget() instanceof RootCallTarget root) { - if (root.getRootNode() instanceof ContinuationRootNode continuation) { - frame = continuation.findFrame(frame); - } + private static Frame resolveFrame(FrameInstance frameInstance, FrameAccess access) { + Frame frame = frameInstance.getFrame(access); + if (frameInstance.getCallTarget() instanceof RootCallTarget root && root.getRootNode() instanceof ContinuationRootNode continuation) { + frame = continuation.findFrame(frame); } return frame; } From 9c1533b1bb0160e75f5a87302aee601663ed42ed Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Tue, 23 Sep 2025 15:24:06 -0400 Subject: [PATCH 2/8] Fix: ContinuationRootNodeImpl should delegate to more RootNode methods --- .../generator/BytecodeRootNodeElement.java | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index d17570b1496c..c7a585d7e63e 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -541,7 +541,13 @@ final class BytecodeRootNodeElement extends CodeTypeElement { abstractBytecodeNode.add(new CodeVariableElement(Set.of(VOLATILE), arrayOf(type(byte.class)), "oldBytecodes")); } - // this should be at the end after all methods have been added. + /* + * These calls should occur after all methods have been added. They use the generated root + * node's method set to determine what method delegate/stub methods to generate. + */ + if (model.hasYieldOperation()) { + continuationRootNodeImpl.addRootNodeDelegateMethods(); + } if (model.enableSerialization) { addMethodStubsToSerializationRootNode(); } @@ -18976,9 +18982,6 @@ void lazyInit() { this.add(createIsCloningAllowed()); this.add(createIsCloneUninitializedSupported()); this.addOptional(createPrepareForCompilation()); - // Should appear last. Uses current method set to determine which methods need to be - // implemented. - this.addAll(createRootNodeProxyMethods()); } private CodeExecutableElement createExecute() { @@ -19143,14 +19146,20 @@ private CodeExecutableElement createPrepareForCompilation() { return ex; } - private List createRootNodeProxyMethods() { - List result = new ArrayList<>(); - - List existing = ElementFilter.methodsIn(continuationRootNodeImpl.getEnclosedElements()); + private void addRootNodeDelegateMethods() { + List existing = ElementFilter.methodsIn(this.getEnclosedElements()); List excludes = List.of( + // Not supported (see isCloningAllowed, isCloneUninitializedSupported). ElementUtils.findMethod(types.RootNode, "copy"), - ElementUtils.findMethod(types.RootNode, "cloneUninitialized")); + ElementUtils.findMethod(types.RootNode, "cloneUninitialized"), + // User code can only obtain a continuation root by executing a yield. + // Parsing is done at this point, so the root is already prepared. + ElementUtils.findMethod(types.RootNode, "prepareForCall"), + // The instrumenter should already know about/have instrumented the root + // node by the time we try to instrument a continuation root. + ElementUtils.findMethod(types.RootNode, "isInstrumentable"), + ElementUtils.findMethod(types.RootNode, "prepareForInstrumentation")); outer: for (ExecutableElement rootNodeMethod : ElementUtils.getOverridableMethods((TypeElement) types.RootNode.asElement())) { // Exclude methods we have already implemented. @@ -19165,33 +19174,31 @@ private List createRootNodeProxyMethods() { continue outer; } } - // Only proxy methods overridden by the template class or its parents. - ExecutableElement templateMethod = ElementUtils.findOverride(rootNodeMethod, model.templateType); + // Only delegate to methods overridden by the generated class or its parents. + ExecutableElement templateMethod = ElementUtils.findOverride(rootNodeMethod, BytecodeRootNodeElement.this); if (templateMethod == null) { continue outer; } - CodeExecutableElement proxyMethod = GeneratorUtils.override(templateMethod); - CodeTreeBuilder b = proxyMethod.createBuilder(); + CodeExecutableElement delegateMethod = GeneratorUtils.override(templateMethod); + CodeTreeBuilder b = delegateMethod.createBuilder(); - boolean isVoid = ElementUtils.isVoid(proxyMethod.getReturnType()); + boolean isVoid = ElementUtils.isVoid(delegateMethod.getReturnType()); if (isVoid) { b.startStatement(); } else { b.startReturn(); } - b.startCall("root", rootNodeMethod.getSimpleName().toString()); - for (VariableElement param : rootNodeMethod.getParameters()) { + b.startCall("root", templateMethod.getSimpleName().toString()); + for (VariableElement param : templateMethod.getParameters()) { b.variable(param); } b.end(); // call b.end(); // statement / return - result.add(proxyMethod); + this.add(delegateMethod); } - - return result; } } From ea608d2cd9d67cddeaa836cdb40f9738f70f7f0b Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Fri, 17 Oct 2025 11:54:02 -0400 Subject: [PATCH 3/8] Fix ContinuationRootNodeImpl.findBytecodeIndex and extend stack trace tests --- .../api/bytecode/test/StackTraceTest.java | 264 ++++++++++++++++-- .../generator/BytecodeRootNodeElement.java | 14 + 2 files changed, 255 insertions(+), 23 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java index 808df07448db..d1f8bd42ef31 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java @@ -63,22 +63,28 @@ import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.TruffleStackTrace; import com.oracle.truffle.api.TruffleStackTraceElement; import com.oracle.truffle.api.bytecode.BytecodeConfig; +import com.oracle.truffle.api.bytecode.BytecodeLocal; import com.oracle.truffle.api.bytecode.BytecodeNode; import com.oracle.truffle.api.bytecode.BytecodeParser; import com.oracle.truffle.api.bytecode.BytecodeRootNode; import com.oracle.truffle.api.bytecode.BytecodeRootNodes; import com.oracle.truffle.api.bytecode.ConstantOperand; +import com.oracle.truffle.api.bytecode.ContinuationResult; import com.oracle.truffle.api.bytecode.GenerateBytecode; import com.oracle.truffle.api.bytecode.GenerateBytecodeTestVariants; import com.oracle.truffle.api.bytecode.GenerateBytecodeTestVariants.Variant; +import com.oracle.truffle.api.bytecode.Instruction; import com.oracle.truffle.api.bytecode.Operation; import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameInstance; +import com.oracle.truffle.api.frame.FrameInstanceVisitor; import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; @@ -166,10 +172,7 @@ public void testThrow() { Assert.fail(); } catch (TestException e) { List elements = TruffleStackTrace.getStackTrace(e); - assertEquals(nodes.length, elements.size()); - for (int i = 0; i < nodes.length; i++) { - assertStackElement(elements.get(i), nodes[i], false); - } + assertStackElements(nodes, elements, "c.ThrowError", "c.Call", false); } } } @@ -192,10 +195,7 @@ public void testThrowBehindInterop() { Assert.fail(); } catch (TestException e) { List elements = TruffleStackTrace.getStackTrace(e); - assertEquals(nodes.length, elements.size()); - for (int i = 0; i < nodes.length; i++) { - assertStackElement(elements.get(i), nodes[i], false); - } + assertStackElements(nodes, elements, "c.ThrowErrorBehindInterop", "c.Call", false); } } } @@ -216,10 +216,7 @@ public void testCapture() { for (int repeat = 0; repeat < REPEATS; repeat++) { List elements = (List) outer.getCallTarget().call(); - assertEquals(nodes.length, elements.size()); - for (int i = 0; i < nodes.length; i++) { - assertStackElement(elements.get(i), nodes[i], false); - } + assertStackElements(nodes, elements, "c.CaptureStack", "c.Call", false); } } @@ -244,15 +241,112 @@ public void testCaptureWithSources() { for (int repeat = 0; repeat < REPEATS; repeat++) { List elements = (List) outer.getCallTarget().call(); - assertEquals(nodes.length, elements.size()); - for (int i = 0; i < nodes.length; i++) { - assertStackElement(elements.get(i), nodes[i], true); - } + assertStackElements(nodes, elements, "c.CaptureStack", "c.Call", true); } } - private void assertStackElement(TruffleStackTraceElement element, StackTraceTestRootNode target, boolean checkSources) { - assertSame(target.getCallTarget(), element.getTarget()); + @Test + @SuppressWarnings("unchecked") + public void testValidateFrameInstances() { + int depth = run.depth; + StackTraceTestRootNode[] nodesConstant = new StackTraceTestRootNode[run.depth]; + StackTraceTestRootNode[] nodes = chainCalls(depth, b -> { + b.beginRoot(); + b.emitDummy(); + b.emitValidateFrameInstances(run, nodesConstant, "c.Call"); + b.endRoot(); + }, true, false); + StackTraceTestRootNode outer = nodes[nodes.length - 1]; + System.arraycopy(nodes, 0, nodesConstant, 0, depth); + + for (int repeat = 0; repeat < REPEATS; repeat++) { + outer.getCallTarget().call(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testCaptureResumed() { + int dept = run.depth; + StackTraceTestRootNode[] nodes = chainResumes(dept, b -> { + b.beginRoot(); + b.beginYield(); + b.emitLoadNull(); + b.endYield(); + b.beginReturn(); + b.emitCaptureStack(); + b.endReturn(); + b.endRoot(); + }, false); + StackTraceTestRootNode outer = nodes[nodes.length - 1]; + + for (int repeat = 0; repeat < REPEATS; repeat++) { + ContinuationResult outerCont = (ContinuationResult) outer.getCallTarget().call(); + List elements = (List) outerCont.continueWith(null); + assertStackElements(nodes, elements, "c.CaptureStack", "c.Resume", false); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testCaptureResumedWithSources() { + int dept = run.depth; + Source s = Source.newBuilder(BytecodeDSLTestLanguage.ID, "root0", "root0.txt").build(); + StackTraceTestRootNode[] nodes = chainResumes(dept, b -> { + b.beginSource(s); + b.beginSourceSection(0, "root0".length()); + b.beginRoot(); + b.beginYield(); + b.emitLoadNull(); + b.endYield(); + b.beginReturn(); + b.emitCaptureStack(); + b.endReturn(); + b.endRoot(); + b.endSourceSection(); + b.endSource(); + }, true); + StackTraceTestRootNode outer = nodes[nodes.length - 1]; + + for (int repeat = 0; repeat < REPEATS; repeat++) { + ContinuationResult outerCont = (ContinuationResult) outer.getCallTarget().call(); + List elements = (List) outerCont.continueWith(null); + assertStackElements(nodes, elements, "c.CaptureStack", "c.Resume", true); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testValidateResumedFrameInstances() { + int depth = run.depth; + StackTraceTestRootNode[] nodesConstant = new StackTraceTestRootNode[run.depth]; + StackTraceTestRootNode[] nodes = chainResumes(depth, b -> { + b.beginRoot(); + b.beginYield(); + b.emitLoadNull(); + b.endYield(); + b.emitValidateFrameInstances(run, nodesConstant, "c.Resume"); + b.endRoot(); + }, false); + StackTraceTestRootNode outer = nodes[nodes.length - 1]; + System.arraycopy(nodes, 0, nodesConstant, 0, depth); + + for (int repeat = 0; repeat < REPEATS; repeat++) { + ContinuationResult outerCont = (ContinuationResult) outer.getCallTarget().call(); + outerCont.continueWith(null); + } + } + + private void assertStackElements(StackTraceTestRootNode[] nodes, List elements, String firstElementInstruction, String chainedElementInstruction, boolean checkSources) { + assertEquals(nodes.length, elements.size()); + assertStackElement(elements.get(0), nodes[0], firstElementInstruction, checkSources); + for (int i = 1; i < nodes.length; i++) { + assertStackElement(elements.get(i), nodes[i], chainedElementInstruction, checkSources); + } + } + + private void assertStackElement(TruffleStackTraceElement element, StackTraceTestRootNode target, String instruction, boolean checkSources) { + assertEquals(target, run.interpreter.variant.cast(element.getTarget())); assertNotNull(element.getLocation()); BytecodeNode bytecode = target.getBytecodeNode(); if (run.interpreter.cached) { @@ -261,7 +355,7 @@ private void assertStackElement(TruffleStackTraceElement element, StackTraceTest } else { assertSame(bytecode, element.getLocation()); } - assertEquals(bytecode.getInstructionsAsList().get(1).getBytecodeIndex(), element.getBytecodeIndex()); + assertEquals(getBytecodeIndexOfFirstInstruction(bytecode, instruction), element.getBytecodeIndex()); Object interopObject = element.getGuestObject(); InteropLibrary lib = InteropLibrary.getFactory().create(interopObject); @@ -276,7 +370,16 @@ private void assertStackElement(TruffleStackTraceElement element, StackTraceTest } catch (UnsupportedMessageException ex) { fail("Interop object could not receive message: " + ex); } + } + private static int getBytecodeIndexOfFirstInstruction(BytecodeNode bytecode, String instructionName) { + for (Instruction i : bytecode.getInstructions()) { + if (instructionName.equals(i.getName())) { + return i.getBytecodeIndex(); + } + } + fail("No instruction found matching instruction name " + instructionName); + return -1; } @Test @@ -345,15 +448,64 @@ private StackTraceTestRootNode[] chainCalls(int depth, BytecodeParser innerParser, boolean includeSources) { + // Constructs a chain of methods that call the previous method, then yield, and then resume + // the previous method. + StackTraceTestRootNode[] nodes = new StackTraceTestRootNode[depth]; + nodes[0] = parse(innerParser); + nodes[0].setName("root0"); + for (int i = 1; i < depth; i++) { + int index = i; + String name = "root" + i; + Source s = includeSources ? Source.newBuilder(BytecodeDSLTestLanguage.ID, name, name + ".txt").build() : null; + nodes[i] = parse(b -> { + // x = prev_root() + // yield + // resume(x) + if (includeSources) { + b.beginSource(s); + b.beginSourceSection(0, name.length()); + } + b.beginRoot(); + BytecodeLocal calleeContinuation = b.createLocal(); + b.beginStoreLocal(calleeContinuation); + b.emitCall(nodes[index - 1].getCallTarget()); + b.endStoreLocal(); + + b.beginYield(); + b.emitLoadConstant(index); + b.endYield(); + + b.beginReturn(); + b.beginResume(); + b.emitLoadLocal(calleeContinuation); + b.emitLoadNull(); + b.endResume(); + b.endReturn(); + b.endRoot().depth = index; + if (includeSources) { + b.endSourceSection(); + b.endSource(); + } + }); + nodes[i].setName(name); + } + return nodes; + } + @GenerateBytecodeTestVariants({ @Variant(suffix = "CachedDefault", configuration = // - @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = false, enableUncachedInterpreter = false, boxingEliminationTypes = {int.class})), + @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = false, enableUncachedInterpreter = false, enableYield = true, boxingEliminationTypes = { + int.class})), @Variant(suffix = "UncachedDefault", configuration = // - @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = false, enableUncachedInterpreter = true, boxingEliminationTypes = {int.class})), + @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = false, enableUncachedInterpreter = true, enableYield = true, boxingEliminationTypes = { + int.class})), @Variant(suffix = "CachedBciInFrame", configuration = // - @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = true, enableUncachedInterpreter = false, boxingEliminationTypes = {int.class})), + @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = true, enableUncachedInterpreter = false, enableYield = true, boxingEliminationTypes = { + int.class})), @Variant(suffix = "UncachedBciInFrame", configuration = // - @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = true, enableUncachedInterpreter = true, boxingEliminationTypes = {int.class})) + @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, storeBytecodeIndexInFrame = true, enableUncachedInterpreter = true, enableYield = true, boxingEliminationTypes = { + int.class})) }) public abstract static class StackTraceTestRootNode extends DebugBytecodeRootNode implements BytecodeRootNode { @@ -405,6 +557,14 @@ static Object doDefault(CallTarget target) { } } + @Operation(storeBytecodeIndex = true) + static final class Resume { + @Specialization + static Object doDefault(ContinuationResult cont, Object resumeValue, @Bind Node node) { + return cont.getContinuationCallTarget().call(node, cont.getFrame(), resumeValue); + } + } + @Operation(storeBytecodeIndex = true) static final class ThrowErrorBehindInterop { @@ -445,6 +605,64 @@ static Object doDefault(@Bind Node node) { return TruffleStackTrace.getStackTrace(ex); } } + + @SuppressWarnings("truffle-interpreted-performance") + @Operation(storeBytecodeIndex = true) + // The parser should populate this array after parsing. This is a hack to expose the roots. + @ConstantOperand(type = Run.class, name = "run") + @ConstantOperand(type = StackTraceTestRootNode[].class, name = "rootNodes") + @ConstantOperand(type = String.class, name = "chainedInstruction") + static final class ValidateFrameInstances { + + @Specialization + static void doDefault(Run run, StackTraceTestRootNode[] rootNodes, String chainedInstruction) { + FrameInstanceValidator visitor = new FrameInstanceValidator(run, rootNodes, chainedInstruction); + Truffle.getRuntime().iterateFrames(visitor); + visitor.checkFinished(); + } + } + + static final class FrameInstanceValidator implements FrameInstanceVisitor { + public final Run run; + public final StackTraceTestRootNode[] targets; + public final String chainedFrameInstruction; + public int index = 0; + + FrameInstanceValidator(Run run, StackTraceTestRootNode[] targets, String chainedFrameInstruction) { + this.run = run; + this.targets = targets; + this.chainedFrameInstruction = chainedFrameInstruction; + } + + public Void visitFrame(FrameInstance frameInstance) { + if (index >= targets.length) { + fail("Visited too many frames."); + } + StackTraceTestRootNode target = targets[index]; + + assertEquals(target, run.interpreter.variant.cast(frameInstance.getCallTarget())); + BytecodeNode bytecode = target.getBytecodeNode(); + if (index != 0) { + assertNotNull(frameInstance.getCallNode()); + if (run.interpreter.cached) { + assertSame(bytecode, BytecodeNode.get(frameInstance.getCallNode())); + assertSame(bytecode, BytecodeNode.get(frameInstance)); + } else { + assertSame(bytecode, frameInstance.getCallNode()); + } + assertEquals(getBytecodeIndexOfFirstInstruction(bytecode, chainedFrameInstruction), frameInstance.getBytecodeIndex()); + } + index++; + return null; + } + + public void checkFinished() { + if (index != targets.length) { + fail("Did not visit every frame. Index is " + index + " and there are " + targets.length + " expected frames."); + } + } + + } } @ExportLibrary(InteropLibrary.class) diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index c7a585d7e63e..83631ce5831e 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -18981,6 +18981,7 @@ void lazyInit() { // RootNode overrides. this.add(createIsCloningAllowed()); this.add(createIsCloneUninitializedSupported()); + this.add(createFindBytecodeIndex()); this.addOptional(createPrepareForCompilation()); } @@ -19130,6 +19131,19 @@ private CodeExecutableElement createIsCloneUninitializedSupported() { return ex; } + private CodeExecutableElement createFindBytecodeIndex() { + CodeExecutableElement ex = GeneratorUtils.override(types.RootNode, "findBytecodeIndex", new String[]{"node", "frame"}); + CodeTreeBuilder b = ex.createBuilder(); + b.startReturn().startCall("root", "findBytecodeIndex"); + b.string("node"); + // unwrap the frame from the continuation frame + b.startGroup().string("frame == null ? null : "); + b.startCall("findFrame").string("frame").end(); + b.end(); + b.end(2); + return ex; + } + private CodeExecutableElement createPrepareForCompilation() { if (!model.enableUncachedInterpreter) { return null; From e7aba1521272987cb34ec5d8b4e26197765e24f3 Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Thu, 25 Sep 2025 14:46:51 -0400 Subject: [PATCH 4/8] Fix: stop overriding countsTowardsStackTraceLimit. The default `countsTowardsStackTraceLimit` impl invokes `isInternal`, which used to force materialization of sources in the Bytecode DSL. To prevent this eager materialization, we overrode `countsTowardsStackTraceLimit` to `true` when the user did not implement it. This is incorrect: the user could override `isInternal`, but the `countsTowardsStackTraceLimit` override would ignore it. Since eager materialization no longer occurs, we can stop generating an override for this method. --- .../api/bytecode/test/StackTraceTest.java | 57 +++++++++++++++++++ .../generator/BytecodeRootNodeElement.java | 23 -------- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java index d1f8bd42ef31..dd937966a1b2 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java @@ -47,8 +47,10 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.graalvm.polyglot.Context; @@ -337,6 +339,40 @@ public void testValidateResumedFrameInstances() { } } + @Test + @SuppressWarnings("unchecked") + public void testCaptureWithLimit() { + int depth = run.depth; + assumeTrue("Cannot test stack trace limits with less than two frames in the trace.", depth >= 2); + int stackTraceLimit = depth / 2; + StackTraceTestRootNode[] nodes = chainCalls(depth, b -> { + b.beginRoot(); + b.emitDummy(); + b.beginReturn(); + b.emitCaptureStackWithLimit(stackTraceLimit); + b.endReturn(); + b.endRoot(); + }, true, false); + StackTraceTestRootNode outer = nodes[nodes.length - 1]; + + // First, invoke as usual. The stack trace should consist of the top stackTraceLimit frames. + StackTraceTestRootNode[] topFrames = Arrays.copyOfRange(nodes, 0, stackTraceLimit); + for (int repeat = 0; repeat < REPEATS; repeat++) { + List elements = (List) outer.getCallTarget().call(); + assertStackElements(topFrames, elements, "c.CaptureStackWithLimit", "c.Call", false); + } + + // Next, mark the top (depth - stackTraceLimit) roots as internal so they do not "count". + for (int i = 0; i < depth - stackTraceLimit; i++) { + nodes[i].internal = true; + } + // Since they do not "count", we should capture every frame. + for (int repeat = 0; repeat < REPEATS; repeat++) { + List elements = (List) outer.getCallTarget().call(); + assertStackElements(nodes, elements, "c.CaptureStackWithLimit", "c.Call", false); + } + } + private void assertStackElements(StackTraceTestRootNode[] nodes, List elements, String firstElementInstruction, String chainedElementInstruction, boolean checkSources) { assertEquals(nodes.length, elements.size()); assertStackElement(elements.get(0), nodes[0], firstElementInstruction, checkSources); @@ -515,6 +551,7 @@ protected StackTraceTestRootNode(BytecodeDSLTestLanguage language, FrameDescript public String name; public int depth; + public boolean internal = false; public void setName(String name) { this.name = name; @@ -525,6 +562,11 @@ public String getName() { return name; } + @Override + public boolean isInternal() { + return internal; + } + @Override public String toString() { return "StackTest[name=" + name + ", depth=" + depth + "]"; @@ -606,6 +648,17 @@ static Object doDefault(@Bind Node node) { } } + @Operation(storeBytecodeIndex = true) + @ConstantOperand(type = int.class, name = "stackTraceLimit") + static final class CaptureStackWithLimit { + + @Specialization + static Object doDefault(int stackTraceLimit, @Bind Node node) { + TestException ex = new TestException(node, stackTraceLimit); + return TruffleStackTrace.getStackTrace(ex); + } + } + @SuppressWarnings("truffle-interpreted-performance") @Operation(storeBytecodeIndex = true) // The parser should populate this array after parsing. This is a hack to expose the roots. @@ -689,6 +742,10 @@ static class TestException extends AbstractTruffleException { super(resolveLocation(location)); } + TestException(Node location, int stackTraceElementLimit) { + super(null, null, stackTraceElementLimit, resolveLocation(location)); + } + private static Node resolveLocation(Node location) { if (location == null) { return null; diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index 83631ce5831e..45e2d2960e03 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -500,7 +500,6 @@ final class BytecodeRootNodeElement extends CodeTypeElement { this.add(createInvalidate()); this.add(createGetRootNodes()); - this.addOptional(createCountTowardsStackTraceLimit()); this.add(createGetSourceSection()); CodeExecutableElement translateStackTraceElement = this.addOptional(createTranslateStackTraceElement()); if (translateStackTraceElement != null) { @@ -1137,28 +1136,6 @@ private CodeExecutableElement createCreateStackTraceElement() { return ex; } - private CodeExecutableElement createCountTowardsStackTraceLimit() { - ExecutableElement executable = ElementUtils.findOverride(ElementUtils.findMethod(types.RootNode, "countsTowardsStackTraceLimit"), model.templateType); - if (executable != null) { - return null; - } - CodeExecutableElement ex = overrideImplementRootNodeMethod(model, "countsTowardsStackTraceLimit"); - if (ex.getModifiers().contains(Modifier.FINAL)) { - // already overridden by the root node. - return null; - } - - ex.getModifiers().remove(Modifier.ABSTRACT); - ex.getModifiers().add(Modifier.FINAL); - CodeTreeBuilder b = ex.createBuilder(); - /* - * We do override with false by default to avoid materialization of sources during stack - * walking. - */ - b.returnTrue(); - return ex; - } - private CodeExecutableElement createGetSourceSection() { CodeExecutableElement ex = GeneratorUtils.override(types.Node, "getSourceSection"); CodeTreeBuilder b = ex.createBuilder(); From bc63fb10445f24ababde66b632d843e215d5cc31 Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Fri, 3 Oct 2025 16:03:43 -0400 Subject: [PATCH 5/8] Fix: Bytecode DSL parser should detect bad root node overrides in parent classes; code generation should not delegate to super method if they are abstract. --- .../bytecode/test/error_tests/ErrorTests.java | 100 ++++++++++++- .../generator/BytecodeRootNodeElement.java | 12 +- .../bytecode/parser/BytecodeDSLParser.java | 134 ++++++++++++------ .../dsl/processor/java/ElementUtils.java | 26 +++- 4 files changed, 213 insertions(+), 59 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/error_tests/ErrorTests.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/error_tests/ErrorTests.java index 7bd0d2d0c4fd..03fc4e88aeb9 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/error_tests/ErrorTests.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/error_tests/ErrorTests.java @@ -367,7 +367,7 @@ protected BadOverrides(ErrorLanguage language, FrameDescriptor frameDescriptor) super(language, frameDescriptor); } - private static final String ERROR_MESSAGE_DELEGATED = "This method is overridden by the generated Bytecode DSL class, so it cannot be declared final."; + private static final String ERROR_MESSAGE_DELEGATED = "This method is overridden by the generated Bytecode DSL class. It cannot be declared final."; private static final String ERROR_MESSAGE = ERROR_MESSAGE_DELEGATED + " You can remove the final modifier to resolve this issue, but since the override will make this method unreachable, it is recommended to simply remove it."; @@ -389,7 +389,7 @@ public final int findBytecodeIndex(Node node, Frame frame) { return 0; } - @ExpectError(ERROR_MESSAGE) + @ExpectError(ERROR_MESSAGE_DELEGATED) @Override public final Node findInstrumentableCallNode(Node callNode, Frame frame, int bytecodeIndex) { return null; @@ -401,7 +401,7 @@ public final boolean isInstrumentable() { return false; } - @ExpectError(ERROR_MESSAGE) + @ExpectError(ERROR_MESSAGE + " Bytecode DSL interpreters should use GenerateBytecode#captureFramesForTrace instead.") @Override public final boolean isCaptureFramesForTrace(boolean compiledFrame) { return false; @@ -459,6 +459,11 @@ public final BytecodeRootNodes getRootNodes() { * methods. The generated code should respect the wider visibility (otherwise, a compiler error * will occur). */ + @ExpectWarning({ + "Method isInstrumentable() in supertype com.oracle.truffle.api.bytecode.test.error_tests.ErrorTests.RootNodeWithOverrides is overridden by the generated Bytecode DSL class." + + " You can suppress this warning by re-declaring the method as abstract in this class.", + "Method prepareForInstrumentation(Set>) in supertype com.oracle.truffle.api.bytecode.test.error_tests.ErrorTests.RootNodeWithMoreOverrides is overridden by the generated Bytecode DSL class." + + " You can suppress this warning by re-declaring the method as abstract in this class."}) @GenerateBytecode(languageClass = ErrorLanguage.class, enableTagInstrumentation = true) public abstract static class AcceptableOverrides extends RootNodeWithMoreOverrides implements BytecodeRootNode { protected AcceptableOverrides(ErrorLanguage language, FrameDescriptor frameDescriptor) { @@ -473,19 +478,47 @@ static int add(int x, int y) { } } - @ExpectWarning("This method is overridden by the generated Bytecode DSL class, so this definition is unreachable and can be removed.") + @ExpectWarning("This method is overridden by the generated Bytecode DSL class. It is unreachable and can be removed.") @Override public int findBytecodeIndex(Node node, Frame frame) { return super.findBytecodeIndex(node, frame); } - @ExpectWarning("This method is overridden by the generated Bytecode DSL class, so this definition is unreachable and can be removed.") + @ExpectWarning("This method is overridden by the generated Bytecode DSL class. It is unreachable and can be removed. Bytecode DSL interpreters should use GenerateBytecode#captureFramesForTrace instead.") @Override public boolean isCaptureFramesForTrace(boolean compiledFrame) { return super.isCaptureFramesForTrace(compiledFrame); } } + // Unlike AcceptableOverrides, this class suppresses warnings about inherited overrides by + // redeclaring them as abstract. + @GenerateBytecode(languageClass = ErrorLanguage.class, enableTagInstrumentation = true) + public abstract static class SuppressedOverrideWarnings extends RootNodeWithMoreOverrides implements BytecodeRootNode { + + protected SuppressedOverrideWarnings(ErrorLanguage language, FrameDescriptor frameDescriptor) { + super(language, frameDescriptor); + } + + @Operation + public static final class Add { + @Specialization + static int add(int x, int y) { + return x + y; + } + } + + @Override + protected abstract Node findInstrumentableCallNode(Node callNode, Frame frame, int bytecodeIndex); + + @Override + public abstract boolean isInstrumentable(); + + @Override + public abstract void prepareForInstrumentation(Set> tags); + + } + private abstract static class RootNodeWithOverrides extends RootNode { protected RootNodeWithOverrides(ErrorLanguage language, FrameDescriptor frameDescriptor) { super(language, frameDescriptor); @@ -517,6 +550,63 @@ public void prepareForInstrumentation(Set> tags) { super.prepareForInstrumentation(tags); } + @Override + protected boolean prepareForCompilation(boolean rootCompilation, int compilationTier, boolean lastTier) { + return super.prepareForCompilation(rootCompilation, compilationTier, lastTier); + } + + } + + @GenerateBytecode(languageClass = ErrorLanguage.class, enableTagInstrumentation = true) + @ExpectError({ + "Method findInstrumentableCallNode(Node, Frame, int) in supertype com.oracle.truffle.api.bytecode.test.error_tests.ErrorTests.RootNodeWithFinalOverrides is overridden by the generated Bytecode DSL class." + + " It cannot be declared final.", + "Method prepareForInstrumentation(Set>) in supertype com.oracle.truffle.api.bytecode.test.error_tests.ErrorTests.RootNodeWithFinalOverrides is overridden by the generated Bytecode DSL class." + + " It cannot be declared final.", + "Method isCaptureFramesForTrace(boolean) in supertype com.oracle.truffle.api.bytecode.test.error_tests.ErrorTests.RootNodeWithFinalOverrides is overridden by the generated Bytecode DSL class." + + " It cannot be declared final. Bytecode DSL interpreters should use GenerateBytecode#captureFramesForTrace instead." + }) + public abstract static class InheritsFinalOverrides extends RootNodeWithFinalOverrides implements BytecodeRootNode { + + protected InheritsFinalOverrides(ErrorLanguage language, FrameDescriptor frameDescriptor) { + super(language, frameDescriptor); + } + + @Operation + public static final class Add { + @Specialization + static int add(int x, int y) { + return x + y; + } + } + + } + + private abstract static class RootNodeWithFinalOverrides extends RootNode { + protected RootNodeWithFinalOverrides(ErrorLanguage language, FrameDescriptor frameDescriptor) { + super(language, frameDescriptor); + } + + @Override + protected final Node findInstrumentableCallNode(Node callNode, Frame frame, int bytecodeIndex) { + return super.findInstrumentableCallNode(callNode, frame, bytecodeIndex); + } + + @Override + protected final boolean isCaptureFramesForTrace(boolean compiledFrame) { + return super.isCaptureFramesForTrace(compiledFrame); + } + + @Override + public final void prepareForInstrumentation(Set> tags) { + super.prepareForInstrumentation(tags); + } + + // This one is OK, we only override if not final + @Override + protected final Object translateStackTraceElement(TruffleStackTraceElement element) { + return super.translateStackTraceElement(element); + } } @ExpectError("The used type system is invalid. Fix errors in the type system first.") diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index 45e2d2960e03..88f7253ddf0b 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -904,7 +904,13 @@ private CodeExecutableElement createFindInstrumentableCallNode() { CodeTreeBuilder b = ex.createBuilder(); b.startDeclaration(types.BytecodeNode, "bc").startStaticCall(types.BytecodeNode, "get").string("callNode").end().end(); b.startIf().string("bc == null || !(bc instanceof AbstractBytecodeNode bytecodeNode)").end().startBlock(); - b.startReturn().string("super.findInstrumentableCallNode(callNode, frame, bytecodeIndex)").end(); + ExecutableElement superImpl = ElementUtils.findMethodInClassHierarchy(ElementUtils.findMethod(types.RootNode, "findInstrumentableCallNode"), model.templateType); + if (superImpl.getModifiers().contains(ABSTRACT)) { + // edge case: root node could redeclare findInstrumentableCallNode as abstract. + b.startReturn().string("null").end(); + } else { + b.startReturn().string("super.findInstrumentableCallNode(callNode, frame, bytecodeIndex)").end(); + } b.end(); b.statement("return bytecodeNode.findInstrumentableCallNode(bytecodeIndex)"); return ex; @@ -1705,8 +1711,8 @@ private CodeExecutableElement createPrepareForCompilation() { // Disable compilation for the uncached interpreter. b.string("bytecode.getTier() != ").staticReference(types.BytecodeTier, "UNCACHED"); - ExecutableElement parentImpl = ElementUtils.findOverride(ElementUtils.findMethod(types.RootNode, "prepareForCompilation", 3), model.templateType); - if (parentImpl != null) { + ExecutableElement parentImpl = ElementUtils.findMethodInClassHierarchy(ElementUtils.findMethod(types.RootNode, "prepareForCompilation", 3), model.templateType); + if (parentImpl != null && !parentImpl.getModifiers().contains(ABSTRACT)) { // Delegate to the parent impl. b.string(" && ").startCall("super.prepareForCompilation").variables(ex.getParameters()).end(); } diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java index f98d3f006fb9..6fef709c448e 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java @@ -387,53 +387,7 @@ private void parseBytecodeDSLModel(TypeElement typeElement, BytecodeDSLModel mod model.interceptInternalException = ElementUtils.findMethod(typeElement, "interceptInternalException"); model.interceptTruffleException = ElementUtils.findMethod(typeElement, "interceptTruffleException"); - // Detect method implementations that will be overridden by the generated class. - List overrides = new ArrayList<>(List.of( - ElementUtils.findMethod(types.RootNode, "execute"), - ElementUtils.findMethod(types.RootNode, "computeSize"), - ElementUtils.findMethod(types.RootNode, "findBytecodeIndex"), - ElementUtils.findMethod(types.RootNode, "findInstrumentableCallNode"), - ElementUtils.findMethod(types.RootNode, "isInstrumentable"), - ElementUtils.findMethod(types.RootNode, "isCaptureFramesForTrace"), - ElementUtils.findMethod(types.RootNode, "prepareForCall"), - ElementUtils.findMethod(types.RootNode, "prepareForInstrumentation"), - ElementUtils.findMethod(types.BytecodeRootNode, "getBytecodeNode"), - ElementUtils.findMethod(types.BytecodeRootNode, "getRootNodes"), - ElementUtils.findMethod(types.BytecodeOSRNode, "executeOSR"), - ElementUtils.findMethod(types.BytecodeOSRNode, "getOSRMetadata"), - ElementUtils.findMethod(types.BytecodeOSRNode, "setOSRMetadata"), - ElementUtils.findMethod(types.BytecodeOSRNode, "storeParentFrameInArguments"), - ElementUtils.findMethod(types.BytecodeOSRNode, "restoreParentFrameFromArguments"))); - - for (ExecutableElement override : overrides) { - ExecutableElement declared = ElementUtils.findMethod(typeElement, override.getSimpleName().toString()); - if (declared == null) { - continue; - } - - if (declared.getModifiers().contains(Modifier.FINAL)) { - model.addError(declared, - "This method is overridden by the generated Bytecode DSL class, so it cannot be declared final. " + - "You can remove the final modifier to resolve this issue, but since the override will make this method unreachable, it is recommended to simply remove it."); - } else { - model.addWarning(declared, "This method is overridden by the generated Bytecode DSL class, so this definition is unreachable and can be removed."); - } - } - - List overridesWithDelegation = new ArrayList<>(List.of( - ElementUtils.findMethod(types.RootNode, "prepareForCompilation"))); - - for (ExecutableElement override : overridesWithDelegation) { - ExecutableElement declared = ElementUtils.findMethod(typeElement, override.getSimpleName().toString()); - if (declared == null) { - continue; - } - - if (declared.getModifiers().contains(Modifier.FINAL)) { - model.addError(declared, "This method is overridden by the generated Bytecode DSL class, so it cannot be declared final."); - } - } - + checkRootNodeOverrides(typeElement, model); if (model.hasErrors()) { return; } @@ -710,6 +664,92 @@ private void parseBytecodeDSLModel(TypeElement typeElement, BytecodeDSLModel mod return; } + /** + * Detect method implementations that will be overridden by the generated class and report an + * appropriate message. + */ + private void checkRootNodeOverrides(TypeElement typeElement, BytecodeDSLModel model) { + List overrides = new ArrayList<>(List.of( + ElementUtils.findMethod(types.RootNode, "execute"), + ElementUtils.findMethod(types.RootNode, "computeSize"), + ElementUtils.findMethod(types.RootNode, "findBytecodeIndex"), + ElementUtils.findMethod(types.RootNode, "isInstrumentable"), + ElementUtils.findMethod(types.RootNode, "prepareForCall"), + ElementUtils.findMethod(types.RootNode, "prepareForInstrumentation"), + ElementUtils.findMethod(types.BytecodeRootNode, "getBytecodeNode"), + ElementUtils.findMethod(types.BytecodeRootNode, "getRootNodes"), + ElementUtils.findMethod(types.BytecodeOSRNode, "executeOSR"), + ElementUtils.findMethod(types.BytecodeOSRNode, "getOSRMetadata"), + ElementUtils.findMethod(types.BytecodeOSRNode, "setOSRMetadata"), + ElementUtils.findMethod(types.BytecodeOSRNode, "storeParentFrameInArguments"), + ElementUtils.findMethod(types.BytecodeOSRNode, "restoreParentFrameFromArguments"))); + + for (ExecutableElement override : overrides) { + checkRootNodeOverride(typeElement, override, model, false, null); + } + + String captureFramesForTraceMessage = String.format("Bytecode DSL interpreters should use %s#captureFramesForTrace instead.", getSimpleName(types.GenerateBytecode)); + checkRootNodeOverride(typeElement, ElementUtils.findInstanceMethod(context.getTypeElement(types.RootNode), "isCaptureFramesForTrace", new TypeMirror[]{context.getType(boolean.class)}), model, + false, captureFramesForTraceMessage); + + List overridesWithDelegation = new ArrayList<>(List.of( + ElementUtils.findMethod(types.RootNode, "findInstrumentableCallNode"), + ElementUtils.findMethod(types.RootNode, "prepareForCompilation"))); + + for (ExecutableElement override : overridesWithDelegation) { + checkRootNodeOverride(typeElement, override, model, true, null); + } + } + + private void checkRootNodeOverride(TypeElement typeElement, ExecutableElement rootNodeMethod, BytecodeDSLModel model, boolean delegated, String customMessage) { + ExecutableElement override = ElementUtils.findOverride(rootNodeMethod, typeElement); + if (override == null || ElementUtils.typeEquals(override.getEnclosingElement().asType(), types.RootNode) || override.getModifiers().contains(Modifier.ABSTRACT)) { + return; + } + boolean inherited = !override.getEnclosingElement().equals(typeElement); + + Element messageElement; + String message; + if (inherited) { + messageElement = typeElement; + message = String.format("Method %s in supertype %s", ElementUtils.getReadableSignature(override), override.getEnclosingElement()); + } else { + messageElement = override; + message = "This method"; + } + message += " is overridden by the generated Bytecode DSL class."; + + if (override.getModifiers().contains(Modifier.FINAL)) { + model.addError(messageElement, message + " " + badFinalOverrideMessage(delegated, inherited, customMessage)); + } else if (!delegated) { + model.addWarning(messageElement, message + " " + badOverrideMessage(inherited, customMessage)); + } + } + + private static String badFinalOverrideMessage(boolean delegated, boolean inherited, String customMessage) { + String message = "It cannot be declared final."; + if (!delegated && !inherited) { + message += " You can remove the final modifier to resolve this issue, but since the override will make this method unreachable, it is recommended to simply remove it."; + } + if (customMessage != null) { + message += " " + customMessage; + } + return message; + } + + private static String badOverrideMessage(boolean inherited, String customMessage) { + String message; + if (inherited) { + message = "You can suppress this warning by re-declaring the method as abstract in this class."; + } else { + message = "It is unreachable and can be removed."; + } + if (customMessage != null) { + message += " " + customMessage; + } + return message; + } + private void resolveBoxingElimination(BytecodeDSLModel model, List manualQuickenings) { /* * If boxing elimination is enabled and the language uses operations with statically known diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/java/ElementUtils.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/java/ElementUtils.java index 7fc3dcf07aaf..4b59cef70a91 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/java/ElementUtils.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/java/ElementUtils.java @@ -2249,12 +2249,30 @@ public static List newElementList(List src) { return workaround; } + /** + * Searches the superclass hierarchy of {@code type} for an override of {@code method}, which + * belongs to the base class. Returns {@code null} if the resolved override is the original + * method. + */ public static ExecutableElement findOverride(ExecutableElement method, TypeElement type) { + ExecutableElement override = findMethodInClassHierarchy(method, type); + if (override != null && !elementEquals(method, override)) { + return override; + } + return null; + } + + /** + * Searches the superclass hierarchy of {@code type} for the most concrete implementation of + * {@code method}. + */ + public static ExecutableElement findMethodInClassHierarchy(ExecutableElement method, TypeElement type) { TypeElement searchType = type; - while (searchType != null && !elementEquals(method.getEnclosingElement(), searchType)) { - ExecutableElement override = findInstanceMethod(searchType, method.getSimpleName().toString(), method.getParameters().stream().map(VariableElement::asType).toArray(TypeMirror[]::new)); - if (override != null) { - return override; + while (searchType != null) { + ExecutableElement instanceMethod = findInstanceMethod(searchType, method.getSimpleName().toString(), + method.getParameters().stream().map(VariableElement::asType).toArray(TypeMirror[]::new)); + if (instanceMethod != null) { + return instanceMethod; } searchType = castTypeElement(searchType.getSuperclass()); } From f932a26fa40ccc6ab59379c2a64d306403e9543a Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Fri, 10 Oct 2025 16:27:25 -0400 Subject: [PATCH 6/8] Fix: Assert that call node used in stack traces is not a BytecodeRootNode. --- .../test/BytecodeDSLTestLanguage.java | 17 ++++++ .../api/bytecode/test/StackTraceTest.java | 60 +++++++++++++++++++ .../BasicInterpreterTest.java | 12 +--- .../api/bytecode/BytecodeLocation.java | 9 +-- .../truffle/api/bytecode/BytecodeNode.java | 1 + .../generator/BytecodeRootNodeElement.java | 3 +- 6 files changed, 82 insertions(+), 20 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/BytecodeDSLTestLanguage.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/BytecodeDSLTestLanguage.java index 1e686b040d7b..0d79831b5237 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/BytecodeDSLTestLanguage.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/BytecodeDSLTestLanguage.java @@ -40,12 +40,15 @@ */ package com.oracle.truffle.api.bytecode.test; +import org.graalvm.polyglot.Context; + import com.oracle.truffle.api.TruffleLanguage; import com.oracle.truffle.api.instrumentation.ProvidedTags; import com.oracle.truffle.api.instrumentation.StandardTags.ExpressionTag; import com.oracle.truffle.api.instrumentation.StandardTags.RootBodyTag; import com.oracle.truffle.api.instrumentation.StandardTags.RootTag; import com.oracle.truffle.api.instrumentation.StandardTags.StatementTag; +import com.oracle.truffle.tck.tests.TruffleTestAssumptions; /** * Placeholder language for Bytecode DSL test interpreters. @@ -60,5 +63,19 @@ protected Object createContext(Env env) { return new Object(); } + /** + * Ensures compilation is disabled (when supported). This allows tests to validate the behaviour + * of assertions, which are optimized away in compiled code. + */ + public static Context createPolyglotContextWithCompilationDisabled() { + var builder = Context.newBuilder(ID); + if (TruffleTestAssumptions.isOptimizingRuntime()) { + builder.option("engine.Compilation", "false"); + } + Context result = builder.build(); + result.enter(); + return result; + } + public static final LanguageReference REF = LanguageReference.create(BytecodeDSLTestLanguage.class); } diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java index dd937966a1b2..1314346c55b1 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/StackTraceTest.java @@ -45,6 +45,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -87,6 +88,7 @@ import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.FrameInstanceVisitor; +import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.ArityException; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; @@ -97,6 +99,7 @@ import com.oracle.truffle.api.library.ExportMessage; import com.oracle.truffle.api.nodes.EncapsulatingNodeReference; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.Source; @RunWith(Parameterized.class) @@ -450,6 +453,52 @@ private static void assertStackElementNoLocation(TruffleStackTraceElement elemen assertNull(BytecodeNode.get(element)); } + @Test + public void testBadLocationFrameInstance() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + CallTarget collectBytecodeNodes = new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + List bytecodeNodes = new ArrayList<>(); + Truffle.getRuntime().iterateFrames(f -> { + bytecodeNodes.add(BytecodeNode.get(f)); + return null; + }); + return bytecodeNodes; + } + }.getCallTarget(); + + StackTraceTestRootNode caller = parse(b -> { + b.beginRoot(); + b.emitCallWithBadLocation(collectBytecodeNodes); + b.endRoot(); + }); + + assertThrows(AssertionError.class, () -> caller.getCallTarget().call()); + } + } + + @Test + public void testBadLocationTruffleStackTraceElement() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + StackTraceTestRootNode capturesStack = parse(b -> { + b.beginRoot(); + b.beginReturn(); + b.emitCaptureStack(); + b.endReturn(); + b.endRoot(); + }); + + StackTraceTestRootNode caller = parse(b -> { + b.beginRoot(); + b.emitCallWithBadLocation(capturesStack.getCallTarget()); + b.endRoot(); + }); + + assertThrows(AssertionError.class, () -> caller.getCallTarget().call()); + } + } + private StackTraceTestRootNode[] chainCalls(int depth, BytecodeParser innerParser, boolean includeLocation, boolean includeSources) { StackTraceTestRootNode[] nodes = new StackTraceTestRootNode[depth]; nodes[0] = parse(innerParser); @@ -599,6 +648,17 @@ static Object doDefault(CallTarget target) { } } + @Operation(storeBytecodeIndex = true) + @ConstantOperand(type = CallTarget.class) + static final class CallWithBadLocation { + @Specialization + static Object doDefault(CallTarget target, @Bind StackTraceTestRootNode root) { + // This is incorrect. The tests using this operation exert paths that should trigger + // assertions. + return target.call(root); + } + } + @Operation(storeBytecodeIndex = true) static final class Resume { @Specialization diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreterTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreterTest.java index dd5e4b3ee91d..99e543202744 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreterTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreterTest.java @@ -1406,7 +1406,7 @@ public void testMaterializedFrameAccessesDeadVariable() { assumeTrue(run.storesBciInFrame()); // This test relies on an assertion. Explicitly open a context with compilation disabled. - try (Context c = createContextWithCompilationDisabled()) { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { BytecodeRootNodes nodes = createNodes(BytecodeConfig.DEFAULT, b -> { b.beginRoot(); @@ -1480,16 +1480,6 @@ public void testMaterializedFrameAccessesDeadVariable() { } - private static Context createContextWithCompilationDisabled() { - var builder = Context.newBuilder(BytecodeDSLTestLanguage.ID); - if (TruffleTestAssumptions.isOptimizingRuntime()) { - builder.option("engine.Compilation", "false"); - } - Context result = builder.build(); - result.enter(); - return result; - } - /* * In this test we check that accessing a local from the wrong frame throws an assertion. */ diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeLocation.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeLocation.java index 4472985a735e..f858f5bbf20d 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeLocation.java +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeLocation.java @@ -278,14 +278,7 @@ public static BytecodeLocation get(FrameInstance frameInstance) { * into the frame before any operation that might call another node. This incurs a bit of * overhead during regular execution (but just for the uncached interpreter). */ - Node location = frameInstance.getCallNode(); - BytecodeNode foundBytecodeNode = null; - for (Node current = location; current != null; current = current.getParent()) { - if (current instanceof BytecodeNode bytecodeNode) { - foundBytecodeNode = bytecodeNode; - break; - } - } + BytecodeNode foundBytecodeNode = BytecodeNode.get(frameInstance); if (foundBytecodeNode == null) { return null; } diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java index ec204d8bc11d..db0a4817b55b 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java @@ -1289,6 +1289,7 @@ private static Frame resolveFrame(FrameInstance frameInstance, FrameAccess acces */ @TruffleBoundary public static BytecodeNode get(FrameInstance frameInstance) { + assert !(frameInstance.getCallNode() instanceof BytecodeRootNode) : "A BytecodeRootNode should not be used as a call location."; return get(frameInstance.getCallNode()); } diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index 88f7253ddf0b..f6a4fd69cdff 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -865,6 +865,7 @@ private Element createFindBytecodeIndex() { CodeExecutableElement ex = overrideImplementRootNodeMethod(model, "findBytecodeIndex", new String[]{"node", "frame"}); mergeSuppressWarnings(ex, "hiding"); CodeTreeBuilder b = ex.createBuilder(); + b.startAssert().string("!(node instanceof ").type(types.BytecodeRootNode).string("): ").doubleQuote("A BytecodeRootNode should not be used as a call location.").end(); if (model.storeBciInFrame) { b.startIf().string("node == null").end().startBlock(); b.statement("return -1"); @@ -882,7 +883,7 @@ private Element createFindBytecodeIndex() { b.declaration(types.Node, "prev", "node"); b.declaration(types.Node, "current", "node"); b.startWhile().string("current != null").end().startBlock(); - b.startIf().string("current ").instanceOf(abstractBytecodeNode.asType()).string(" b").end().startBlock(); + b.startIf().string("current").instanceOf(abstractBytecodeNode.asType()).string(" b").end().startBlock(); b.statement("bytecode = b"); b.statement("break"); b.end(); From d77c5e2462cb27c208fe743f36e9b51336fb58ae Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Wed, 24 Sep 2025 16:18:12 -0400 Subject: [PATCH 7/8] Add BytecodeFrame abstraction and GenerateBytecode#captureFramesForTrace configuration option Adds a new BytecodeFrame abstraction that captures the frame and location information. This abstraction should be preferred over BytecodeNode helpers since it ensures the correct location is used. Also adds a new GenerateBytecode#captureFramesForTrace configuration option that directs the interpreter to capture frames for interpreter use. Previously, the frame would sometimes be available depending on internal interpreter correctness requirements. Now, the frame is reliably available depending on the configuration option. Lanugages should use BytecodeFrame.get(element) to obtain the frame data from a TruffleStackTraceElement (element.getFrame() may still return a non-null value, but its use by languages is not supported). --- .../test/BytecodeDSLCompilationTest.java | 174 +++++++ truffle/CHANGELOG.md | 2 + .../api/bytecode/test/LocalHelpersTest.java | 477 +++++++++++++----- .../AbstractBasicInterpreterTest.java | 12 + .../basic_interpreter/BasicInterpreter.java | 52 ++ .../truffle/api/bytecode/BytecodeFrame.java | 331 ++++++++++++ .../truffle/api/bytecode/BytecodeNode.java | 88 +++- .../api/bytecode/GenerateBytecode.java | 18 + .../truffle/api/TruffleStackTraceElement.java | 5 + .../truffle/dsl/processor/TruffleTypes.java | 2 + .../generator/BytecodeRootNodeElement.java | 76 ++- .../bytecode/model/BytecodeDSLModel.java | 1 + .../bytecode/parser/BytecodeDSLParser.java | 1 + 13 files changed, 1097 insertions(+), 142 deletions(-) create mode 100644 truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeFrame.java diff --git a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/BytecodeDSLCompilationTest.java b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/BytecodeDSLCompilationTest.java index ce6aa48a12a7..e052ee46ee67 100644 --- a/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/BytecodeDSLCompilationTest.java +++ b/compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/BytecodeDSLCompilationTest.java @@ -27,6 +27,7 @@ import static com.oracle.truffle.api.bytecode.test.basic_interpreter.AbstractBasicInterpreterTest.createNodes; import static com.oracle.truffle.api.bytecode.test.basic_interpreter.AbstractBasicInterpreterTest.parseNode; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -44,11 +45,13 @@ import org.junit.runners.Parameterized.Parameters; import com.oracle.truffle.api.bytecode.BytecodeConfig; +import com.oracle.truffle.api.bytecode.BytecodeFrame; import com.oracle.truffle.api.bytecode.BytecodeLocal; import com.oracle.truffle.api.bytecode.BytecodeLocation; import com.oracle.truffle.api.bytecode.BytecodeNode; import com.oracle.truffle.api.bytecode.BytecodeParser; import com.oracle.truffle.api.bytecode.BytecodeRootNodes; +import com.oracle.truffle.api.bytecode.BytecodeTier; import com.oracle.truffle.api.bytecode.ContinuationResult; import com.oracle.truffle.api.bytecode.test.BytecodeDSLTestLanguage; import com.oracle.truffle.api.bytecode.test.basic_interpreter.AbstractBasicInterpreterTest; @@ -56,6 +59,7 @@ import com.oracle.truffle.api.bytecode.test.basic_interpreter.BasicInterpreter; import com.oracle.truffle.api.bytecode.test.basic_interpreter.BasicInterpreterBuilder; import com.oracle.truffle.api.bytecode.test.basic_interpreter.BasicInterpreterBuilder.BytecodeVariant; +import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.FrameSlotKind; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.instrumentation.ExecutionEventNode; @@ -957,6 +961,176 @@ public void testTagInstrumentation() { assertCompiled(target); } + @Test + public void testCaptureFrame() { + BytecodeRootNodes rootNodes = createNodes(run, BytecodeDSLTestLanguage.REF.get(null), BytecodeConfig.DEFAULT, b -> { + b.beginRoot(); + b.beginReturn(); + b.beginCaptureFrame(); + b.emitLoadArgument(0); + b.emitLoadArgument(1); + b.endCaptureFrame(); + b.endReturn(); + BasicInterpreter callee = b.endRoot(); + callee.setName("callee"); + + b.beginRoot(); + BytecodeLocal x = b.createLocal(); + b.beginStoreLocal(x); + b.emitLoadConstant(123); + b.endStoreLocal(); + b.beginInvoke(); + b.emitLoadConstant(callee); + b.emitLoadArgument(0); + b.emitLoadArgument(1); + b.endInvoke(); + b.endRoot().setName("caller"); + }); + BasicInterpreter caller = rootNodes.getNode(1); + + OptimizedCallTarget target = (OptimizedCallTarget) caller.getCallTarget(); + + // The callee frame (the top of the stack) should never be accessible. + assertNull(target.call(0, FrameInstance.FrameAccess.READ_ONLY)); + + // In the interpreter the caller frame should always be accessible. + assertNotCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.READ_ONLY), false); + assertNotCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.READ_WRITE), false); + assertNotCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.MATERIALIZE), false); + + // Force transition to cached. + caller.getBytecodeNode().setUncachedThreshold(0); + target.call(0, FrameInstance.FrameAccess.READ_ONLY); + assertEquals(BytecodeTier.CACHED, caller.getBytecodeNode().getTier()); + + // In compiled code the caller frame should always be accessible, but may be a copy. + // Requesting the frame should not invalidate compiled code. + target.compile(true); + assertCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.READ_ONLY), true); + assertCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.READ_WRITE), false); + assertCompiled(target); + checkCallerBytecodeFrame((BytecodeFrame) target.call(1, FrameInstance.FrameAccess.MATERIALIZE), false); + assertCompiled(target); + } + + @Test + public void testCaptureNonVirtualFrame() { + BytecodeRootNodes rootNodes = createNodes(run, BytecodeDSLTestLanguage.REF.get(null), BytecodeConfig.DEFAULT, b -> { + b.beginRoot(); + b.beginReturn(); + b.beginCaptureNonVirtualFrame(); + b.emitLoadArgument(0); + b.endCaptureNonVirtualFrame(); + b.endReturn(); + BasicInterpreter callee = b.endRoot(); + callee.setName("callee"); + + b.beginRoot(); + BytecodeLocal x = b.createLocal(); + b.beginStoreLocal(x); + b.emitLoadConstant(123); + b.endStoreLocal(); + b.beginInvoke(); + b.emitLoadConstant(callee); + b.emitLoadArgument(0); + b.endInvoke(); + b.endRoot().setName("caller"); + }); + BasicInterpreter caller = rootNodes.getNode(1); + + OptimizedCallTarget target = (OptimizedCallTarget) caller.getCallTarget(); + + // The callee frame (the top of the stack) should never be accessible. + assertNull(target.call(0)); + + // In the interpreter the non-virtual caller frame should be accessible. + assertNotCompiled(target); + BytecodeFrame nonVirtualFrame = (BytecodeFrame) target.call(1); + assertNotCompiled(target); + checkCallerBytecodeFrame(nonVirtualFrame, false); + + // Force transition to cached. + caller.getBytecodeNode().setUncachedThreshold(0); + target.call(0); + assertEquals(BytecodeTier.CACHED, caller.getBytecodeNode().getTier()); + + // In compiled code the non-virtual caller frame should be inaccessible. + target.compile(true); + assertCompiled(target); + assertNull(target.call(1)); + assertCompiled(target); + } + + @Test + public void testCaptureNonVirtualFrameAfterMaterialization() { + BytecodeRootNodes rootNodes = createNodes(run, BytecodeDSLTestLanguage.REF.get(null), BytecodeConfig.DEFAULT, b -> { + b.beginRoot(); + b.beginReturn(); + b.beginCaptureNonVirtualFrame(); + b.emitLoadArgument(0); + b.endCaptureNonVirtualFrame(); + b.endReturn(); + BasicInterpreter callee = b.endRoot(); + callee.setName("callee"); + + b.beginRoot(); + BytecodeLocal x = b.createLocal(); + b.beginStoreLocal(x); + b.emitLoadConstant(123); + b.endStoreLocal(); + + b.beginBlackhole(); + b.emitMaterializeFrame(); // force materialize frame. + b.endBlackhole(); + + b.beginInvoke(); + b.emitLoadConstant(callee); + b.emitLoadArgument(0); + b.endInvoke(); + b.endRoot().setName("caller"); + }); + BasicInterpreter caller = rootNodes.getNode(1); + + OptimizedCallTarget target = (OptimizedCallTarget) caller.getCallTarget(); + + // The callee frame (the top of the stack) should never be accessible. + assertNull(target.call(0)); + + // In the interpreter the non-virtual caller frame should be accessible. + assertNotCompiled(target); + BytecodeFrame nonVirtualFrame = (BytecodeFrame) target.call(1); + assertNotCompiled(target); + checkCallerBytecodeFrame(nonVirtualFrame, false); + + // Force transition to cached. + caller.getBytecodeNode().setUncachedThreshold(0); + target.call(0); + assertEquals(BytecodeTier.CACHED, caller.getBytecodeNode().getTier()); + + // In compiled code the frame should be accessible because it was materialized already. + target.compile(true); + assertCompiled(target); + nonVirtualFrame = (BytecodeFrame) target.call(1); + checkCallerBytecodeFrame(nonVirtualFrame, false); + assertCompiled(target); + } + + private void checkCallerBytecodeFrame(BytecodeFrame bytecodeFrame, boolean isCopy) { + assertNotNull(bytecodeFrame); + assertEquals(1, bytecodeFrame.getLocalCount()); + if (isCopy || AbstractBasicInterpreterTest.hasRootScoping(run.interpreterClass())) { + assertEquals(123, bytecodeFrame.getLocalValue(0)); + } else { + // the local gets cleared on exit. + assertEquals(AbstractBasicInterpreterTest.getDefaultLocalValue(run.interpreterClass()), bytecodeFrame.getLocalValue(0)); + } + } + @TruffleInstrument.Registration(id = BytecodeDSLCompilationTestInstrumentation.ID, services = Instrumenter.class) public static class BytecodeDSLCompilationTestInstrumentation extends TruffleInstrument { diff --git a/truffle/CHANGELOG.md b/truffle/CHANGELOG.md index 1bab0733a35b..7a5f2b4a35d2 100644 --- a/truffle/CHANGELOG.md +++ b/truffle/CHANGELOG.md @@ -38,6 +38,8 @@ This changelog summarizes major changes between Truffle versions relevant to lan * GR-70086: Added `replacementOf` and `replacementMethod` attributes to `GenerateLibrary.Abstract` annotation. They enable automatic generation of legacy delegators during message library evolution, while allowing custom conversions when needed. * GR-70086 Deprecated `Message.resolve(Class, String)`. Use `Message.resolveExact(Class, String, Class...)` with argument types instead. This deprecation was necessary as library messages are no longer unique by message name, if the previous message was deprecated. +* GR-69861: Bytecode DSL: Added a `BytecodeFrame` abstraction for capturing frame state and accessing frame data. This abstraction should be preferred over `BytecodeNode` access methods because it captures the correct interpreter location data. +* GR-69861: Bytecode DSL: Added a `captureFramesForTrace` parameter to `@GenerateBytecode` that enables capturing of frames in `TruffleStackTraceElement`s. Previously, frame data was unreliably available in stack traces; now, it is guaranteed to be available if requested. Languages must use the `BytecodeFrame` abstraction to access frame data from `TruffleStackTraceElement`s rather than access the frame directly. ## Version 25.0 * GR-31495 Added ability to specify language and instrument specific options using `Source.Builder.option(String, String)`. Languages may describe available source options by implementing `TruffleLanguage.getSourceOptionDescriptors()` and `TruffleInstrument.getSourceOptionDescriptors()` respectively. diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java index 083f75cdd122..eb4545a7a5c9 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/LocalHelpersTest.java @@ -51,7 +51,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import org.graalvm.polyglot.Context; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -65,7 +67,10 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.Truffle; +import com.oracle.truffle.api.TruffleStackTrace; + import com.oracle.truffle.api.bytecode.BytecodeConfig; +import com.oracle.truffle.api.bytecode.BytecodeFrame; import com.oracle.truffle.api.bytecode.BytecodeLocal; import com.oracle.truffle.api.bytecode.BytecodeNode; import com.oracle.truffle.api.bytecode.BytecodeParser; @@ -85,6 +90,7 @@ import com.oracle.truffle.api.dsl.Bind; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.FrameDescriptor; import com.oracle.truffle.api.frame.FrameInstance; @@ -94,6 +100,7 @@ import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.IndirectCallNode; +import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.nodes.UnexpectedResultException; @@ -133,6 +140,11 @@ private boolean hasBoxingElimination() { interpreterClass == BytecodeNodeWithLocalIntrospectionWithBEIllegal.class; } + private boolean capturesFrameForTrace() { + Class interpreterClass = bytecode.getGeneratedClass(); + return interpreterClass != BytecodeNodeWithLocalIntrospectionBaseNoCapturedFrames.class; + } + public BytecodeRootNodes parseNodes(BytecodeParser builder) { return bytecode.create(null, BytecodeConfig.DEFAULT, builder); } @@ -882,36 +894,140 @@ public void testSetLocalUsingBytecodeLocalIndex() { } @Test - public void testGetLocalsSimpleStacktrace() { + public void testCreateMaterializedBytecodeFrame() { /* @formatter:off - * - * def bar() { - * y = 42 - * z = "hello" - * - * } - * - * def foo() { - * x = 123 - * } - * - * @formatter:on - */ - CallTarget collectFrames = new RootNode(null) { - @Override - public Object execute(VirtualFrame frame) { - List frames = new ArrayList<>(); - Truffle.getRuntime().iterateFrames(f -> { - frames.add(f); - return null; - }); - return frames; - } - }.getCallTarget(); + * + * foo = 42 + * bar = 123 + * yield createMaterializedFrame() + * return (arg0, foo) + * + * @formatter:on + */ + BytecodeNodeWithLocalIntrospection root = parseNode(b -> { + b.beginRoot(); + b.beginBlock(); + BytecodeLocal foo = makeLocal(b, "foo"); + BytecodeLocal bar = makeLocal(b, "bar"); - BytecodeNodeWithLocalIntrospection bar = parseNode(b -> { + b.beginStoreLocal(foo); + b.emitLoadConstant(42); + b.endStoreLocal(); + + b.beginStoreLocal(bar); + b.emitLoadConstant(123); + b.endStoreLocal(); + + b.beginYield(); + b.emitCreateMaterializedBytecodeFrame(); + b.endYield(); + + b.beginReturn(); + b.beginMakePair(); + b.emitLoadArgument(0); + b.emitLoadLocal(foo); + b.endMakePair(); + b.endReturn(); + + b.endBlock(); + b.endRoot(); + }); + + ContinuationResult cont = (ContinuationResult) root.getCallTarget().call(444); + BytecodeFrame bytecodeFrame = (BytecodeFrame) cont.getResult(); + assertEquals(2, bytecodeFrame.getLocalCount()); + assertArrayEquals(new Object[]{"foo", "bar"}, bytecodeFrame.getLocalNames()); + assertEquals(42, bytecodeFrame.getLocalValue(0)); + assertEquals(123, bytecodeFrame.getLocalValue(1)); + assertEquals(1, bytecodeFrame.getArgumentCount()); + assertEquals(444, bytecodeFrame.getArgument(0)); + // Updates to a materialized frame should be visible. + bytecodeFrame.setArgument(0, -444); + assertEquals(-444, bytecodeFrame.getArgument(0)); + bytecodeFrame.setLocalValue(0, -42); + assertEquals(-42, bytecodeFrame.getLocalValue(0)); + assertEquals(new Pair(-444, -42), cont.continueWith(null)); + } + + @Test + public void testCreateCopiedBytecodeFrame() { + /* @formatter:off + * + * foo = 42 + * bar = 123 + * yield createCopiedFrame() + * return (arg0, foo) + * + * @formatter:on + */ + BytecodeNodeWithLocalIntrospection root = parseNode(b -> { b.beginRoot(); + b.beginBlock(); + BytecodeLocal foo = makeLocal(b, "foo"); + BytecodeLocal bar = makeLocal(b, "bar"); + + b.beginStoreLocal(foo); + b.emitLoadConstant(42); + b.endStoreLocal(); + + b.beginStoreLocal(bar); + b.emitLoadConstant(123); + b.endStoreLocal(); + + b.beginYield(); + b.emitCreateCopiedBytecodeFrame(); + b.endYield(); + + b.beginReturn(); + b.beginMakePair(); + b.emitLoadArgument(0); + b.emitLoadLocal(foo); + b.endMakePair(); + b.endReturn(); + + b.endBlock(); + b.endRoot(); + }); + + ContinuationResult cont = (ContinuationResult) root.getCallTarget().call(444); + BytecodeFrame bytecodeFrame = (BytecodeFrame) cont.getResult(); + assertEquals(2, bytecodeFrame.getLocalCount()); + assertArrayEquals(new Object[]{"foo", "bar"}, bytecodeFrame.getLocalNames()); + assertEquals(42, bytecodeFrame.getLocalValue(0)); + assertEquals(123, bytecodeFrame.getLocalValue(1)); + assertEquals(1, bytecodeFrame.getArgumentCount()); + assertEquals(444, bytecodeFrame.getArgument(0)); + // Updates to a copied frame should not be visible. + bytecodeFrame.setArgument(0, -444); + assertEquals(-444, bytecodeFrame.getArgument(0)); + bytecodeFrame.setLocalValue(0, -42); + assertEquals(-42, bytecodeFrame.getLocalValue(0)); + assertEquals(new Pair(444, 42), cont.continueWith(null)); + } + private void doTestBytecodeFrameGet(boolean yield, Function frameCaptured, boolean frameWritable, RootNode collectBytecodeFrames) { + /* @formatter:off + * + * def bar(arg0) { + * y = 42 + * z = "hello" + * if (yield) y = yield 0 + * return collectBytecodeFrames() + * } + * + * def foo(arg0) { + * x = 123 + * if (yield) return continue(bar(444), 43) + * else bar(444) + * } + * + * foo(222) + * + * @formatter:on + */ + + BytecodeNodeWithLocalIntrospection bar = parseNode(b -> { + b.beginRoot(); b.beginBlock(); BytecodeLocal y = makeLocal(b, "y"); @@ -924,14 +1040,21 @@ public Object execute(VirtualFrame frame) { b.emitLoadConstant("hello"); b.endStoreLocal(); + if (yield) { + b.beginStoreLocal(y); + b.beginYield(); + b.emitLoadConstant(0); + b.endYield(); + b.endStoreLocal(); + } + b.beginReturn(); b.beginInvoke(); - b.emitLoadConstant(collectFrames); + b.emitLoadConstant(collectBytecodeFrames.getCallTarget()); b.endInvoke(); b.endReturn(); b.endBlock(); - b.endRoot(); }); @@ -946,9 +1069,17 @@ public Object execute(VirtualFrame frame) { b.endStoreLocal(); b.beginReturn(); + if (yield) { + b.beginContinue(); + } b.beginInvoke(); b.emitLoadConstant(bar); + b.emitLoadConstant(444); b.endInvoke(); + if (yield) { + b.emitLoadConstant(43); + b.endContinue(); + } b.endReturn(); b.endBlock(); @@ -956,129 +1087,185 @@ public Object execute(VirtualFrame frame) { b.endRoot(); }); - Object result = foo.getCallTarget().call(); + Object result = foo.getCallTarget().call(222); assertTrue(result instanceof List); @SuppressWarnings("unchecked") - List frames = (List) result; + List frames = (List) result; assertEquals(3, frames.size()); // - assertNull(BytecodeNode.getLocalValues(frames.get(0))); + assertNull(frames.get(0)); // bar - Object[] barLocals = BytecodeNode.getLocalValues(frames.get(1)); - assertArrayEquals(new Object[]{42, "hello"}, barLocals); - Object[] barLocalNames = BytecodeNode.getLocalNames(frames.get(1)); - assertArrayEquals(new Object[]{"y", "z"}, barLocalNames); - BytecodeNode.setLocalValues(frames.get(1), new Object[]{-42, "goodbye"}); - assertArrayEquals(new Object[]{-42, "goodbye"}, BytecodeNode.getLocalValues(frames.get(1))); + BytecodeFrame barFrame = frames.get(1); + if (frameCaptured.apply(1)) { + assertEquals(2, barFrame.getLocalCount()); + if (yield) { + assertEquals(43, barFrame.getLocalValue(0)); + } else { + assertEquals(42, barFrame.getLocalValue(0)); + } + assertEquals("hello", barFrame.getLocalValue(1)); + if (frameWritable) { + barFrame.setLocalValue(0, -42); + assertEquals(-42, barFrame.getLocalValue(0)); + } + assertArrayEquals(new Object[]{"y", "z"}, barFrame.getLocalNames()); + assertEquals(1, barFrame.getArgumentCount()); + assertEquals(444, barFrame.getArgument(0)); + assertEquals(BytecodeNodeWithLocalIntrospection.FRAME_DESCRIPTOR_INFO, barFrame.getFrameDescriptorInfo()); + } else { + assertNull(barFrame); + } // foo - Object[] fooLocals = BytecodeNode.getLocalValues(frames.get(2)); - assertArrayEquals(new Object[]{123}, fooLocals); - Object[] fooLocalNames = BytecodeNode.getLocalNames(frames.get(2)); - assertArrayEquals(new Object[]{"x"}, fooLocalNames); - BytecodeNode.setLocalValues(frames.get(2), new Object[]{456}); - assertArrayEquals(new Object[]{456}, BytecodeNode.getLocalValues(frames.get(2))); + BytecodeFrame fooFrame = frames.get(2); + if (frameCaptured.apply(2)) { + assertEquals(1, fooFrame.getLocalCount()); + assertEquals(123, fooFrame.getLocalValue(0)); + if (frameWritable) { + barFrame.setLocalValue(0, 456); + assertEquals(456, barFrame.getLocalValue(0)); + } + assertArrayEquals(new Object[]{"x"}, fooFrame.getLocalNames()); + assertEquals(1, fooFrame.getArgumentCount()); + assertEquals(222, fooFrame.getArgument(0)); + assertEquals(BytecodeNodeWithLocalIntrospection.FRAME_DESCRIPTOR_INFO, fooFrame.getFrameDescriptorInfo()); + } else { + assertNull(fooFrame); + } } @Test - public void testGetLocalsContinuationStacktrace() { - /* @formatter:off - * - * def bar() { - * y = yield 0 - * - * } - * - * def foo() { - * x = 123 - * continue(bar(), 42) - * } - * - * @formatter:on - */ - CallTarget collectFrames = new RootNode(null) { + public void testBytecodeFrameGetFrameInstances() { + doTestBytecodeFrameGet(false, LocalHelpersTest::alwaysCaptured, true, new RootNode(null) { @Override public Object execute(VirtualFrame frame) { - List frames = new ArrayList<>(); + List frames = new ArrayList<>(); Truffle.getRuntime().iterateFrames(f -> { - frames.add(f); + frames.add(BytecodeFrame.get(f, FrameInstance.FrameAccess.READ_WRITE)); return null; }); return frames; } - }.getCallTarget(); - - BytecodeNodeWithLocalIntrospection bar = parseNode(b -> { - b.beginRoot(); - - BytecodeLocal y = makeLocal(b, "y"); - - b.beginStoreLocal(y); - b.beginYield(); - b.emitLoadConstant(0); - b.endYield(); - b.endStoreLocal(); - - b.beginReturn(); - b.beginInvoke(); - b.emitLoadConstant(collectFrames); - b.endInvoke(); - b.endReturn(); - - b.endRoot(); }); + } - BytecodeNodeWithLocalIntrospection foo = parseNode(b -> { - b.beginRoot(); - BytecodeLocal x = makeLocal(b, "x"); - - b.beginStoreLocal(x); - b.emitLoadConstant(123); - b.endStoreLocal(); - - b.beginReturn(); - b.beginContinue(); + @Test + public void testBytecodeFrameGetFrameInstancesContinuation() { + doTestBytecodeFrameGet(true, LocalHelpersTest::alwaysCaptured, true, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + List frames = new ArrayList<>(); + Truffle.getRuntime().iterateFrames(f -> { + frames.add(BytecodeFrame.get(f, FrameInstance.FrameAccess.READ_WRITE)); + return null; + }); + return frames; + } + }); + } - b.beginInvoke(); - b.emitLoadConstant(bar); - b.endInvoke(); + @Test + public void testBytecodeFrameGetNonVirtualFrameInstances() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + doTestBytecodeFrameGet(false, LocalHelpersTest::alwaysCaptured, true, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + List frames = new ArrayList<>(); + Truffle.getRuntime().iterateFrames(f -> { + frames.add(BytecodeFrame.getNonVirtual(f)); + return null; + }); + return frames; + } + }); + } + } - b.emitLoadConstant(42); + @Test + public void testBytecodeFrameGetNonVirtualFrameInstancesContinuation() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + doTestBytecodeFrameGet(true, LocalHelpersTest::alwaysCaptured, true, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + List frames = new ArrayList<>(); + Truffle.getRuntime().iterateFrames(f -> { + frames.add(BytecodeFrame.getNonVirtual(f)); + return null; + }); + return frames; + } + }); + } + } - b.endContinue(); - b.endReturn(); + @Test + public void testBytecodeFrameGetTruffleStackTraceElement() { + doTestBytecodeFrameGet(false, unused -> capturesFrameForTrace(), false, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + return getBytecodeFrames(new TestException(this)); + } + }); + } - b.endRoot(); + @Test + public void testBytecodeFrameGetTruffleStackTraceElementContinuation() { + doTestBytecodeFrameGet(true, unused -> capturesFrameForTrace(), false, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + return getBytecodeFrames(new TestException(this)); + } }); + } - Object result = foo.getCallTarget().call(); - assertTrue(result instanceof List); + @Test + public void testBytecodeFrameGetNonVirtualTruffleStackTraceElement() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + // stack trace elements capture read-only copies, so getNonVirtual is always null. + doTestBytecodeFrameGet(false, LocalHelpersTest::neverCaptured, true, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + return getNonVirtualBytecodeFrames(new TestException(this)); + } + }); + } + } - @SuppressWarnings("unchecked") - List frames = (List) result; - assertEquals(3, frames.size()); + @Test + public void testBytecodeFrameGetNonVirtualTruffleStackTraceElementContinuation() { + try (Context c = BytecodeDSLTestLanguage.createPolyglotContextWithCompilationDisabled()) { + // getNonVirtual returns a result for continuation frames because they're materialized. + Function frameCaptured = index -> index == 1 && capturesFrameForTrace(); + doTestBytecodeFrameGet(true, frameCaptured, true, new RootNode(null) { + @Override + public Object execute(VirtualFrame frame) { + return getNonVirtualBytecodeFrames(new TestException(this)); + } + }); + } + } - // - assertNull(BytecodeNode.getLocalValues(frames.get(0))); + @SuppressWarnings("unused") + private static boolean alwaysCaptured(int unused) { + return true; + } - // bar - Object[] barLocals = BytecodeNode.getLocalValues(frames.get(1)); - assertArrayEquals(new Object[]{42}, barLocals); - Object[] barLocalNames = BytecodeNode.getLocalNames(frames.get(1)); - assertArrayEquals(new Object[]{"y"}, barLocalNames); - BytecodeNode.setLocalValues(frames.get(1), new Object[]{-42}); - assertArrayEquals(new Object[]{-42}, BytecodeNode.getLocalValues(frames.get(1))); + @SuppressWarnings("unused") + private static boolean neverCaptured(int unused) { + return false; + } - // foo - Object[] fooLocals = BytecodeNode.getLocalValues(frames.get(2)); - assertArrayEquals(new Object[]{123}, fooLocals); - Object[] fooLocalNames = BytecodeNode.getLocalNames(frames.get(2)); - assertArrayEquals(new Object[]{"x"}, fooLocalNames); - BytecodeNode.setLocalValues(frames.get(2), new Object[]{456}); - assertArrayEquals(new Object[]{456}, BytecodeNode.getLocalValues(frames.get(2))); + @TruffleBoundary + private static List getBytecodeFrames(AbstractTruffleException ex) { + return TruffleStackTrace.getStackTrace(ex).stream().map(BytecodeFrame::get).toList(); + } + + @TruffleBoundary + private static List getNonVirtualBytecodeFrames(AbstractTruffleException ex) { + return TruffleStackTrace.getStackTrace(ex).stream().map(BytecodeFrame::getNonVirtual).toList(); } @Test @@ -1640,51 +1827,62 @@ public void testGetLocalMetadataMaterializedAccessor() { @GenerateBytecodeTestVariants({ @Variant(suffix = "Base", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // enableYield = true, // - enableMaterializedLocalAccesses = true)), + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)), @Variant(suffix = "BaseDefault", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // defaultLocalValue = "DEFAULT", // enableYield = true, // - enableMaterializedLocalAccesses = true)), + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)), + @Variant(suffix = "BaseNoCapturedFrames", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // + enableYield = true, // + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = false)), @Variant(suffix = "WithBEIllegal", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // enableQuickening = true, // enableUncachedInterpreter = true, // boxingEliminationTypes = {boolean.class, long.class}, // enableYield = true, // - enableMaterializedLocalAccesses = true)), + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)), @Variant(suffix = "WithBEIllegalRootScoped", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // enableQuickening = true, // enableUncachedInterpreter = true, // boxingEliminationTypes = {boolean.class, long.class}, // enableBlockScoping = false, // enableYield = true, // - enableMaterializedLocalAccesses = true)), + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)), @Variant(suffix = "WithBEObjectDefault", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // enableQuickening = true, // boxingEliminationTypes = {boolean.class, long.class}, // enableUncachedInterpreter = true, // defaultLocalValue = "resolveDefault()", // enableYield = true, // - enableMaterializedLocalAccesses = true)), + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)), @Variant(suffix = "WithBENullDefault", configuration = @GenerateBytecode(languageClass = BytecodeDSLTestLanguage.class, // enableQuickening = true, // boxingEliminationTypes = {boolean.class, long.class}, // enableUncachedInterpreter = true, // defaultLocalValue = "null", // enableYield = true, // - enableMaterializedLocalAccesses = true)) + enableMaterializedLocalAccesses = true, // + captureFramesForTrace = true)) }) abstract class BytecodeNodeWithLocalIntrospection extends DebugBytecodeRootNode implements BytecodeRootNode { @CompilationFinal public int reservedLocalIndex = -1; static final Object DEFAULT = new Object(); + static final Object FRAME_DESCRIPTOR_INFO = new Object(); static Object resolveDefault() { CompilerAsserts.neverPartOfCompilation("Must be cached and not triggered during compilation."); return DEFAULT; } - protected BytecodeNodeWithLocalIntrospection(BytecodeDSLTestLanguage language, FrameDescriptor frameDescriptor) { - super(language, frameDescriptor); + protected BytecodeNodeWithLocalIntrospection(BytecodeDSLTestLanguage language, FrameDescriptor.Builder frameDescriptorBuilder) { + super(language, frameDescriptorBuilder.info(FRAME_DESCRIPTOR_INFO).build()); } @Operation @@ -2052,6 +2250,22 @@ public static void perform(VirtualFrame frame, Object value, } } + @Operation + public static final class CreateMaterializedBytecodeFrame { + @Specialization + public static BytecodeFrame perform(VirtualFrame frame, @Bind BytecodeNode node, @Bind("$bytecodeIndex") int bci) { + return node.createMaterializedFrame(bci, frame.materialize()); + } + } + + @Operation + public static final class CreateCopiedBytecodeFrame { + @Specialization + public static BytecodeFrame perform(VirtualFrame frame, @Bind BytecodeNode node, @Bind("$bytecodeIndex") int bci) { + return node.createCopiedFrame(bci, frame); + } + } + @Operation public static final class Same { @Specialization @@ -2118,3 +2332,12 @@ public static Pair doMakePair(Object left, Object right) { record Pair(Object left, Object right) { } + +@SuppressWarnings("serial") +class TestException extends AbstractTruffleException { + + TestException(Node location) { + super(location); + } + +} diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/AbstractBasicInterpreterTest.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/AbstractBasicInterpreterTest.java index 8430c1153e81..f090ee6fb891 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/AbstractBasicInterpreterTest.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/AbstractBasicInterpreterTest.java @@ -694,6 +694,18 @@ public static List allVariants() { return BasicInterpreterBuilder.variants(); } + public static boolean hasRootScoping(Class interpreterClass) { + return interpreterClass == BasicInterpreterWithRootScoping.class || + interpreterClass == BasicInterpreterProductionRootScoping.class; + } + + public static Object getDefaultLocalValue(Class interpreterClass) { + if (interpreterClass == BasicInterpreterWithOptimizations.class || interpreterClass == BasicInterpreterWithRootScoping.class) { + return BasicInterpreter.LOCAL_DEFAULT_VALUE; + } + return null; + } + /// Code gen helpers protected static void emitReturn(BasicInterpreterBuilder b, long value) { diff --git a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreter.java b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreter.java index c9b42dea7847..188e488814fa 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreter.java +++ b/truffle/src/com.oracle.truffle.api.bytecode.test/src/com/oracle/truffle/api/bytecode/test/basic_interpreter/BasicInterpreter.java @@ -54,6 +54,7 @@ import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.bytecode.BytecodeConfig; +import com.oracle.truffle.api.bytecode.BytecodeFrame; import com.oracle.truffle.api.bytecode.BytecodeLocation; import com.oracle.truffle.api.bytecode.BytecodeNode; import com.oracle.truffle.api.bytecode.BytecodeRootNode; @@ -82,6 +83,7 @@ import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.InteropLibrary; @@ -778,6 +780,56 @@ public static BytecodeLocation perform(@Bind BytecodeLocation location) { } } + @Operation(storeBytecodeIndex = true) + @SuppressWarnings("truffle-interpreted-performance") + public static final class CaptureFrame { + private static final Object FRAME_UNAVAILABLE = new Object(); + + @Specialization + public static BytecodeFrame perform(int skipFrames, FrameInstance.FrameAccess access) { + Object frameWalkResult = Truffle.getRuntime().iterateFrames(frameInstance -> { + BytecodeFrame result = BytecodeFrame.get(frameInstance, access); + if (result == null) { + // Return a sentinel value so that frame walking doesn't continue. + return FRAME_UNAVAILABLE; + } + return result; + }, skipFrames); + + return frameWalkResult == FRAME_UNAVAILABLE ? null : (BytecodeFrame) frameWalkResult; + } + } + + @Operation(storeBytecodeIndex = true) + @SuppressWarnings("truffle-interpreted-performance") + public static final class CaptureNonVirtualFrame { + private static final Object FRAME_UNAVAILABLE = new Object(); + + @Specialization + public static BytecodeFrame perform(int skipFrames) { + Object frameWalkResult = Truffle.getRuntime().iterateFrames(frameInstance -> { + BytecodeFrame result = BytecodeFrame.getNonVirtual(frameInstance); + if (result == null) { + // Return a sentinel value so that frame walking doesn't continue. + return FRAME_UNAVAILABLE; + } + return result; + }, skipFrames); + + return frameWalkResult == FRAME_UNAVAILABLE ? null : (BytecodeFrame) frameWalkResult; + } + } + + // Special operation that forces its operand to escape. + @Operation + public static final class Blackhole { + @Specialization + @TruffleBoundary + public static void perform(@SuppressWarnings("unused") Object value) { + // do nothing + } + } + @Instrumentation public static final class PrintHere { @Specialization diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeFrame.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeFrame.java new file mode 100644 index 000000000000..99bd98440dde --- /dev/null +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeFrame.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.truffle.api.bytecode; + +import java.util.Arrays; +import java.util.Objects; + +import com.oracle.truffle.api.Truffle; +import com.oracle.truffle.api.TruffleStackTraceElement; +import com.oracle.truffle.api.frame.Frame; +import com.oracle.truffle.api.frame.FrameDescriptor; +import com.oracle.truffle.api.frame.FrameInstance; +import com.oracle.truffle.api.frame.FrameInstance.FrameAccess; + +/** + * Represents a captured Bytecode DSL frame, including the location metadata needed to access the + * data in the frame. + *

+ * {@link BytecodeFrame} is intended for use cases where the frame escapes or outlives the root node + * invocation. Prefer using a built-in operation or {@link LocalAccessor} to access the frame + * whenever possible. + *

+ * There are a few ways to capture the frame: + *

    + *
  • {@link BytecodeNode#createCopiedFrame} captures a copy.
  • + *
  • {@link BytecodeNode#createMaterializedFrame} captures the original frame.
  • + *
  • {@link BytecodeFrame#get(FrameInstance, FrameAccess)} captures a frame from a + * {@link FrameInstance}. It captures the original frame if {@link FrameAccess#READ_WRITE} or + * {@link FrameAccess#MATERIALIZE} is requested. Otherwise, it captures either the original frame or + * a copy.
  • + *
  • {@link BytecodeFrame#get(TruffleStackTraceElement)} captures a frame from a + * {@link TruffleStackTraceElement}. It captures either the original frame or a copy.
  • + *
  • {@link BytecodeFrame#getNonVirtual(FrameInstance)} captures the original frame from a + * {@link FrameInstance}, if it is non-virtual.
  • + *
  • {@link BytecodeFrame#getNonVirtual(TruffleStackTraceElement)} captures the original frame + * from a {@link TruffleStackTraceElement}, if it is non-virtual and available in the stack + * trace.
  • + *
+ * Copied frames do not observe updates made to the original frame. + *

+ * Note: if the interpreter uses {@link GenerateBytecode#enableBlockScoping block scoping}, any + * non-copied {@link BytecodeFrame} is only valid until the interpreter continues execution. The + * frame must not be used after this point; doing so can cause undefined behaviour. + * If you need to access the frame after execution continues, you should capture a copy or + * explicitly {@link #copy()} the captured bytecode frame. This restriction also applies to frames + * created by methods like {@link BytecodeFrame#get(TruffleStackTraceElement)}, which do not specify + * whether they capture the original frame or a copy. + * + * @since 25.1 + */ +public final class BytecodeFrame { + private final Frame frame; + private final BytecodeNode bytecode; + private final int bytecodeIndex; + + BytecodeFrame(Frame frame, BytecodeNode bytecode, int bytecodeIndex) { + assert frame.getFrameDescriptor() == bytecode.getRootNode().getFrameDescriptor(); + this.frame = Objects.requireNonNull(frame); + this.bytecode = bytecode; + this.bytecodeIndex = bytecodeIndex; + } + + /** + * Returns a copy of this frame. This method can be used to snapshot the current state of a + * bytecode frame, in case it may be modified or become invalid in the future. + * + * @return a copy of this frame that is always valid and will not observe updates + * @since 25.1 + */ + public BytecodeFrame copy() { + return new BytecodeFrame(copyFrame(frame), bytecode, bytecodeIndex); + } + + /** + * Returns the bytecode location associated with the captured frame. This location is only valid + * until the bytecode interpreter resumes execution. + * + * @since 25.1 + */ + public BytecodeLocation getLocation() { + return new BytecodeLocation(bytecode, bytecodeIndex); + } + + /** + * Returns the bytecode node associated with the captured frame. + * + * @since 25.1 + */ + public BytecodeNode getBytecodeNode() { + return bytecode; + } + + /** + * Returns the bytecode index associated with the captured frame. + * + * @since 25.1 + */ + public int getBytecodeIndex() { + return bytecodeIndex; + } + + /** + * Returns the number of live locals in the captured frame. + * + * @since 25.1 + */ + public int getLocalCount() { + return bytecode.getLocalCount(bytecodeIndex); + } + + /** + * Returns the value of the local at the given offset. The offset should be between 0 and + * {@link #getLocalCount()}. + * + * @since 25.1 + */ + public Object getLocalValue(int localOffset) { + return bytecode.getLocalValue(bytecodeIndex, frame, localOffset); + } + + /** + * Updates the value of the local at the given offset. The offset should be between 0 and + * {@link #getLocalCount()}. + *

+ * This method will throw an {@link AssertionError} if the captured frame does not support + * writes. + * + * @since 25.1 + */ + public void setLocalValue(int localOffset, Object value) { + bytecode.setLocalValue(bytecodeIndex, frame, localOffset, value); + } + + /** + * Returns the names associated with the live locals, if provided. + * + * @since 25.1 + */ + public Object[] getLocalNames() { + return bytecode.getLocalNames(bytecodeIndex); + } + + /** + * Returns the number of arguments in the captured frame. + * + * @since 25.1 + */ + public int getArgumentCount() { + return frame.getArguments().length; + } + + /** + * Returns the value of the argument at the given index. The offset should be between 0 and + * {@link #getArgumentCount()}. + * + * @since 25.1 + */ + public Object getArgument(int argumentIndex) { + return frame.getArguments()[argumentIndex]; + } + + /** + * Updates the value of the local at the given offset. The offset should be between 0 and + * {@link #getArgumentCount()}. + * + * @since 25.1 + */ + public void setArgument(int argumentIndex, Object value) { + frame.getArguments()[argumentIndex] = value; + } + + /** + * Returns the {@link FrameDescriptor#getInfo() info} object associated with the frame's + * descriptor. + * + * @since 25.1 + */ + public Object getFrameDescriptorInfo() { + return frame.getFrameDescriptor().getInfo(); + } + + /** + * Creates a copy of the given frame. + */ + static Frame copyFrame(Frame frame) { + FrameDescriptor fd = frame.getFrameDescriptor(); + Object[] args = frame.getArguments(); + Frame copiedFrame = Truffle.getRuntime().createMaterializedFrame(Arrays.copyOf(args, args.length), fd); + frame.copyTo(0, copiedFrame, 0, fd.getNumberOfSlots()); + return copiedFrame; + } + + /** + * Creates a bytecode frame from the given frame instance. + * + * @param frameInstance the frame instance + * @param access the access mode to use when capturing the frame + * @return a bytecode frame, or null if the frame instance is missing location info. + * @since 25.1 + */ + public static BytecodeFrame get(FrameInstance frameInstance, FrameInstance.FrameAccess access) { + BytecodeNode bytecode = BytecodeNode.get(frameInstance); + if (bytecode == null) { + return null; + } + Frame frame = bytecode.resolveFrameImpl(frameInstance, access); + int bytecodeIndex = bytecode.findBytecodeIndex(frameInstance); + return new BytecodeFrame(frame, bytecode, bytecodeIndex); + } + + /** + * Attempts to create a bytecode frame from the given frame instance. Returns null if the + * corresponding frame is virtual. The frame can be read from, written to, and escaped. + *

+ * This method can be used to probe for a frame that can safely escape without forcing + * materialization. For example, if a language needs to capture local variables from a stack + * frame, it's often more efficient to use an existing non-virtual frame rather than create a + * copy of all variables. + * + * @param frameInstance the frame instance + * @return a bytecode frame or null if the frame is virtual or if the frame instance is missing + * location info. + * + * @since 25.1 + */ + public static BytecodeFrame getNonVirtual(FrameInstance frameInstance) { + if (frameInstance.isVirtualFrame()) { + return null; + } + BytecodeNode bytecode = BytecodeNode.get(frameInstance); + if (bytecode == null) { + return null; + } + /* + * READ_WRITE returns the original frame. Since it's not virtual it is safe to escape it + * (either we are in the interpreter, or it is already materialized). + */ + Frame frame = bytecode.resolveFrameImpl(frameInstance, FrameAccess.READ_WRITE); + int bytecodeIndex = bytecode.findBytecodeIndex(frameInstance); + return new BytecodeFrame(frame, bytecode, bytecodeIndex); + } + + /** + * Creates a bytecode frame from the given stack trace element. + *

+ * This method will return null unless the interpreter specifies + * {@link GenerateBytecode#captureFramesForTrace}, which indicates whether frames should be + * captured. + * + * @param element the stack trace element + * @return a bytecode frame, or null if the frame was not captured or the stack trace element is + * missing location information. + * @since 25.1 + */ + public static BytecodeFrame get(TruffleStackTraceElement element) { + BytecodeNode bytecode = BytecodeNode.get(element); + if (bytecode == null) { + return null; + } + Frame frame = bytecode.resolveFrameImpl(element); + if (frame == null) { + return null; + } + return new BytecodeFrame(frame, bytecode, element.getBytecodeIndex()); + } + + /** + * Attempts to create a bytecode frame from the given stack trace element. Returns null if the + * corresponding frame is virtual. The frame can be read from, written to, and escaped. + *

+ * This method can be used to probe for a frame that can safely escape without forcing + * materialization. For example, if a language needs to capture local variables from a stack + * frame, it's often more efficient to use an existing non-virtual frame rather than create a + * copy of all variables. + * + * @param element the stack trace element + * @return a bytecode frame or null if the frame is virtual/unavailable or if the frame instance + * is missing location info. + * + * @since 25.1 + */ + public static BytecodeFrame getNonVirtual(TruffleStackTraceElement element) { + BytecodeNode bytecode = BytecodeNode.get(element); + if (bytecode == null) { + return null; + } + Frame frame = bytecode.resolveNonVirtualFrameImpl(element); + if (frame == null) { + return null; + } + return new BytecodeFrame(frame, bytecode, element.getBytecodeIndex()); + } +} diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java index db0a4817b55b..7e8a7f2f1f52 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/BytecodeNode.java @@ -49,7 +49,6 @@ import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; -import com.oracle.truffle.api.RootCallTarget; import com.oracle.truffle.api.TruffleStackTraceElement; import com.oracle.truffle.api.bytecode.Instruction.InstructionIterable; import com.oracle.truffle.api.dsl.Bind; @@ -57,6 +56,7 @@ import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameInstance; import com.oracle.truffle.api.frame.FrameInstance.FrameAccess; +import com.oracle.truffle.api.frame.MaterializedFrame; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.RootNode; @@ -446,6 +446,41 @@ public final BytecodeNode ensureSourceInformation() { */ public abstract TagTree getTagTree(); + /** + * Returns a {@link BytecodeFrame} capturing the materialized interpreter state. Note that + * materialized frames may become invalid once the interpreter resumes; see the + * {@link BytecodeFrame} javadoc for more info. + *

+ * Prefer to capture the frame using this method (rather than {@link #createCopiedFrame}) when + * capturing the frame is a frequent operation or when future updates to the frame should be + * observable. + * + * @param bytecodeIndex the current bytecode index + * @param frame the current materialized frame + * @return the captured bytecode frame + * @since 25.1 + */ + public final BytecodeFrame createMaterializedFrame(int bytecodeIndex, MaterializedFrame frame) { + return new BytecodeFrame(frame, this, bytecodeIndex); + } + + /** + * Returns a {@link BytecodeFrame} capturing a copy of the interpreter state. The copy is always + * valid, but will not observe subsequent changes to the frame. + *

+ * Prefer to capture the frame using this method (rather than {@link #createMaterializedFrame}) + * when capturing the frame is an infrequent operation or when the frame does not need to + * observe future updates. + * + * @param bytecodeIndex the current bytecode index + * @param frame the current frame + * @return the captured bytecode frame + * @since 25.1 + */ + public final BytecodeFrame createCopiedFrame(int bytecodeIndex, Frame frame) { + return new BytecodeFrame(BytecodeFrame.copyFrame(frame), this, bytecodeIndex); + } + /** * Returns a new array containing the current value of each local in the frame. This method * should only be used for slow-path use cases (like frame introspection). Prefer reading locals @@ -1216,8 +1251,9 @@ protected static final Object createDefaultStackTraceElement(TruffleStackTraceEl * {@link com.oracle.truffle.api.frame.FrameInstance frameInstance}. * * @see #getLocalValues(int, Frame) + * @see BytecodeFrame#get(FrameInstance, FrameInstance.FrameAccess) * @param frameInstance the frame instance - * @return a new array of local values, or null if the frame instance does not correspond to an + * @return a new array of local values, or null if the frame instance does not correspond to a * {@link BytecodeRootNode} * @since 24.2 */ @@ -1226,7 +1262,7 @@ public static Object[] getLocalValues(FrameInstance frameInstance) { if (bytecode == null) { return null; } - Frame frame = resolveFrame(frameInstance, FrameAccess.READ_ONLY); + Frame frame = bytecode.resolveFrameImpl(frameInstance, FrameAccess.READ_ONLY); int bci = bytecode.findBytecodeIndexImpl(frame, frameInstance.getCallNode()); return bytecode.getLocalValues(bci, frame); } @@ -1236,8 +1272,9 @@ public static Object[] getLocalValues(FrameInstance frameInstance) { * {@link com.oracle.truffle.api.frame.FrameInstance frameInstance}. * * @see #getLocalNames(int) + * @see BytecodeFrame#get(FrameInstance, FrameInstance.FrameAccess) * @param frameInstance the frame instance - * @return a new array of names, or null if the frame instance does not correspond to an + * @return a new array of names, or null if the frame instance does not correspond to a * {@link BytecodeRootNode} * @since 24.2 */ @@ -1255,6 +1292,7 @@ public static Object[] getLocalNames(FrameInstance frameInstance) { * {@link com.oracle.truffle.api.frame.FrameInstance frameInstance}. * * @see #setLocalValues(int, Frame, Object[]) + * @see BytecodeFrame#get(FrameInstance, FrameInstance.FrameAccess) * @param frameInstance the frame instance * @return whether the locals could be set with the information available in the frame instance * @since 24.2 @@ -1265,16 +1303,41 @@ public static boolean setLocalValues(FrameInstance frameInstance, Object[] value return false; } int bci = bytecode.findBytecodeIndex(frameInstance); - bytecode.setLocalValues(bci, resolveFrame(frameInstance, FrameAccess.READ_WRITE), values); + bytecode.setLocalValues(bci, bytecode.resolveFrameImpl(frameInstance, FrameAccess.READ_WRITE), values); return true; } - private static Frame resolveFrame(FrameInstance frameInstance, FrameAccess access) { - Frame frame = frameInstance.getFrame(access); - if (frameInstance.getCallTarget() instanceof RootCallTarget root && root.getRootNode() instanceof ContinuationRootNode continuation) { - frame = continuation.findFrame(frame); - } - return frame; + /** + * Internal method to be overridden by generated code. + * + * @since 25.1 + */ + protected abstract Frame resolveFrameImpl(FrameInstance frameInstance, FrameInstance.FrameAccess access); + + /** + * Internal method to be overridden by generated code. + *

+ * By default, frames are unavailable in stack trace elements unless + * {@link GenerateBytecode#captureFramesForTrace()} is set. + * + * @since 25.1 + */ + @SuppressWarnings("unused") + protected Frame resolveFrameImpl(TruffleStackTraceElement element) { + return null; + } + + /** + * Internal method to be overridden by generated code. + *

+ * By default, frames are unavailable in stack trace elements unless + * {@link GenerateBytecode#captureFramesForTrace()} is set. + * + * @since 25.1 + */ + @SuppressWarnings("unused") + protected Frame resolveNonVirtualFrameImpl(TruffleStackTraceElement element) { + return null; } /** @@ -1302,8 +1365,7 @@ public static BytecodeNode get(FrameInstance frameInstance) { */ @ExplodeLoop public static BytecodeNode get(Node node) { - Node location = node; - for (Node currentNode = location; currentNode != null; currentNode = currentNode.getParent()) { + for (Node currentNode = node; currentNode != null; currentNode = currentNode.getParent()) { if (currentNode instanceof BytecodeNode bytecodeNode) { return bytecodeNode; } diff --git a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/GenerateBytecode.java b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/GenerateBytecode.java index 0cf8b9ffed01..211cb8e56781 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/GenerateBytecode.java +++ b/truffle/src/com.oracle.truffle.api.bytecode/src/com/oracle/truffle/api/bytecode/GenerateBytecode.java @@ -48,6 +48,7 @@ import com.oracle.truffle.api.CompilerAsserts; import com.oracle.truffle.api.HostCompilerDirectives; import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.TruffleStackTraceElement; import com.oracle.truffle.api.bytecode.debug.BytecodeDebugListener; import com.oracle.truffle.api.frame.Frame; import com.oracle.truffle.api.frame.FrameSlotTypeException; @@ -57,6 +58,7 @@ import com.oracle.truffle.api.instrumentation.StandardTags.RootTag; import com.oracle.truffle.api.interop.NodeLibrary; import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.nodes.RootNode; /** * Generates a bytecode interpreter using the Bytecode DSL. The Bytecode DSL automatically produces @@ -420,6 +422,22 @@ */ boolean storeBytecodeIndexInFrame() default false; + /** + * Whether {@link TruffleStackTraceElement stack trace elements} of the annotated root node + * should capture frames. This flag should be used instead of + * {@link RootNode#isCaptureFramesForTrace(boolean)}, which the Bytecode DSL prevents you from + * overriding. + *

+ * When this flag is non-null, you can use {@link BytecodeFrame#get(TruffleStackTraceElement)} + * to access frame data from a stack trace element. The frame only supports read-only access. + *

+ * Bytecode DSL interpreters must not access frames directly using + * {@link TruffleStackTraceElement#getFrame}. + * + * @since 25.1 + */ + boolean captureFramesForTrace() default false; + /** * Path to a file containing optimization decisions. This file is generated using tracing on a * representative corpus of code. diff --git a/truffle/src/com.oracle.truffle.api/src/com/oracle/truffle/api/TruffleStackTraceElement.java b/truffle/src/com.oracle.truffle.api/src/com/oracle/truffle/api/TruffleStackTraceElement.java index afb835c353fe..49df48f884e4 100644 --- a/truffle/src/com.oracle.truffle.api/src/com/oracle/truffle/api/TruffleStackTraceElement.java +++ b/truffle/src/com.oracle.truffle.api/src/com/oracle/truffle/api/TruffleStackTraceElement.java @@ -146,6 +146,11 @@ public RootCallTarget getTarget() { * Returns the read-only frame. Returns null if the initial {@link RootNode} that * filled in the stack trace did not request frames to be captured by overriding * {@link RootNode#isCaptureFramesForTrace(Node)}. + *

+ * Bytecode DSL note: This method should not be used with Bytecode DSL + * interpreters. See + * {@link com.oracle.truffle.api.bytecode.GenerateBytecode#captureFramesForTrace} for more + * information. * * @since 0.31 */ diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/TruffleTypes.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/TruffleTypes.java index 154460dc2da7..e3293b85da59 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/TruffleTypes.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/TruffleTypes.java @@ -153,6 +153,7 @@ public class TruffleTypes { public static final String Option_Group_Name = "com.oracle.truffle.api.Option.Group"; public static final String Option_Name = "com.oracle.truffle.api.Option"; public static final String Profile_Name = "com.oracle.truffle.api.profiles.Profile"; + public static final String RootCallTarget_Name = "com.oracle.truffle.api.RootCallTarget"; public static final String RootNode_Name = "com.oracle.truffle.api.nodes.RootNode"; public static final String IndirectCallNode_Name = "com.oracle.truffle.api.nodes.IndirectCallNode"; public static final String InlinedProfile_Name = "com.oracle.truffle.api.profiles.InlinedProfile"; @@ -215,6 +216,7 @@ public class TruffleTypes { public final DeclaredType NodeInterface = c.getDeclaredType(NodeInterface_Name); public final DeclaredType NodeUtil = c.getDeclaredType(NodeUtil_Name); public final DeclaredType Profile = c.getDeclaredTypeOptional(Profile_Name); + public final DeclaredType RootCallTarget = c.getDeclaredType(RootCallTarget_Name); public final DeclaredType RootNode = c.getDeclaredType(RootNode_Name); public final DeclaredType IndirectCallNode = c.getDeclaredType(IndirectCallNode_Name); public final DeclaredType InlinedProfile = c.getDeclaredTypeOptional(InlinedProfile_Name); diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java index f6a4fd69cdff..98640f7bd6f7 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/generator/BytecodeRootNodeElement.java @@ -853,10 +853,19 @@ private CodeExecutableElement createContinueAt() { private Element createIsCaptureFramesForTrace() { CodeExecutableElement ex = overrideImplementRootNodeMethod(model, "isCaptureFramesForTrace", new String[]{"compiled"}, new TypeMirror[]{type(boolean.class)}); CodeTreeBuilder b = ex.createBuilder(); - if (model.storeBciInFrame) { + if (model.captureFramesForTrace) { + b.lineComment("GenerateBytecode#captureFramesForTrace is true."); b.statement("return true"); - } else { + } else if (model.storeBciInFrame) { + b.lineComment("GenerateBytecode#storeBytecodeIndexInFrame is true, so the frame is needed for location computations."); + b.statement("return true"); + } else if (model.enableUncachedInterpreter) { + b.lineComment("The uncached interpreter (which is never compiled) needs the frame for location computations."); + b.lineComment("This may capture the frame in more situations than strictly necessary, but doing so in the interpreter is inexpensive."); b.statement("return !compiled"); + } else { + b.lineComment("GenerateBytecode#captureFramesForTrace is not true, and the interpreter does not need the frame for location lookups."); + b.statement("return false"); } return ex; } @@ -12544,6 +12553,12 @@ final class AbstractBytecodeNodeElement extends CodeTypeElement { this.add(createGetLocalInfo()); this.add(createGetLocals()); + this.add(createResolveFrameImplFrameInstance()); + if (model.captureFramesForTrace) { + this.add(createResolveFrameImplTruffleStackTraceElement()); + this.add(createResolveNonVirtualFrameImpl()); + } + if (model.enableTagInstrumentation) { this.add(createGetTagNodes()); } @@ -12955,6 +12970,63 @@ private CodeExecutableElement createGetLocals() { return ex; } + private CodeExecutableElement createResolveFrameImplFrameInstance() { + CodeExecutableElement ex = GeneratorUtils.override(types.BytecodeNode, "resolveFrameImpl", new String[]{"frameInstance", "access"}); + CodeTreeBuilder b = ex.createBuilder(); + + if (model.hasYieldOperation()) { + b.startIf().string("frameInstance.getCallTarget() instanceof ").type(types.RootCallTarget).string(" root && root.getRootNode() instanceof ").type( + continuationRootNodeImpl.asType()).string(" continuation").end().startBlock(); + b.lineComment("Continuations use materialized frames, which support all access modes."); + b.startReturn().startCall("continuation.findFrame").startCall("frameInstance.getFrame"); + b.staticReference(types.FrameInstance_FrameAccess, "READ_ONLY"); + b.end(3); + b.end(); // if + } + b.startReturn().string("frameInstance.getFrame(access)").end(); + return ex; + } + + private CodeExecutableElement createResolveFrameImplTruffleStackTraceElement() { + if (!model.captureFramesForTrace) { + throw new AssertionError("should not generate resolveFrameImpl(TruffleStackTraceElement) if frames are not captured."); + } + CodeExecutableElement ex = GeneratorUtils.override(types.BytecodeNode, "resolveFrameImpl", new String[]{"element"}); + CodeTreeBuilder b = ex.createBuilder(); + + if (model.hasYieldOperation()) { + b.declaration(types.Frame, "frame", "element.getFrame()"); + b.startIf().string("frame != null && element.getTarget().getRootNode() instanceof ").type(continuationRootNodeImpl.asType()).string( + " continuation").end().startBlock(); + b.statement("frame = continuation.findFrame(frame)"); + b.end(); + b.startReturn().string("frame").end(); + } else { + b.startReturn().string("element.getFrame()").end(); + } + return ex; + } + + private CodeExecutableElement createResolveNonVirtualFrameImpl() { + if (!model.captureFramesForTrace) { + throw new AssertionError("should not generate resolveNonVirtualFrameImpl(TruffleStackTraceElement) if frames are not captured."); + } + CodeExecutableElement ex = GeneratorUtils.override(types.BytecodeNode, "resolveNonVirtualFrameImpl", new String[]{"element"}); + CodeTreeBuilder b = ex.createBuilder(); + + if (model.hasYieldOperation()) { + b.declaration(types.Frame, "frame", "element.getFrame()"); + b.startIf().string("frame != null && element.getTarget().getRootNode() instanceof ").type(continuationRootNodeImpl.asType()).string( + " continuation").end().startBlock(); + b.lineComment("Continuation frames are always materialized."); + b.startReturn().string("continuation.findFrame(frame)").end(); + b.end(); + } + b.lineComment("Frames obtained in stack walks are always read-only."); + b.startReturn().string("null").end(); + return ex; + } + record InstructionValidationGroup(List immediates, int instructionLength, boolean allowNegativeChildBci, boolean localVar, boolean localVarMat) { InstructionValidationGroup(BytecodeDSLModel model, InstructionModel instruction) { diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/model/BytecodeDSLModel.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/model/BytecodeDSLModel.java index 4e721c130d27..89c38a322677 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/model/BytecodeDSLModel.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/model/BytecodeDSLModel.java @@ -118,6 +118,7 @@ public BytecodeDSLModel(ProcessorContext context, TypeElement templateType, Anno public boolean enableYield; public boolean enableMaterializedLocalAccesses; public boolean storeBciInFrame; + public boolean captureFramesForTrace; public boolean bytecodeDebugListener; public boolean additionalAssertions; public boolean inlinePrimitiveConstants; diff --git a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java index 6fef709c448e..16de9b31a4b7 100644 --- a/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java +++ b/truffle/src/com.oracle.truffle.dsl.processor/src/com/oracle/truffle/dsl/processor/bytecode/parser/BytecodeDSLParser.java @@ -238,6 +238,7 @@ private void parseBytecodeDSLModel(TypeElement typeElement, BytecodeDSLModel mod model.enableMaterializedLocalAccesses = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "enableMaterializedLocalAccesses"); model.enableYield = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "enableYield"); model.storeBciInFrame = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "storeBytecodeIndexInFrame"); + model.captureFramesForTrace = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "captureFramesForTrace"); model.enableQuickening = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "enableQuickening"); model.enableTagInstrumentation = ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "enableTagInstrumentation"); model.enableRootTagging = model.enableTagInstrumentation && ElementUtils.getAnnotationValue(Boolean.class, generateBytecodeMirror, "enableRootTagging"); From 616f13330245de73f48f039c10e902a92cefecd7 Mon Sep 17 00:00:00 2001 From: Matt D'Souza Date: Wed, 24 Sep 2025 17:20:51 -0400 Subject: [PATCH 8/8] Update signatures --- .../snapshot.sigtest | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/truffle/src/com.oracle.truffle.api.bytecode/snapshot.sigtest b/truffle/src/com.oracle.truffle.api.bytecode/snapshot.sigtest index e182a77bc20f..353dd6c75247 100644 --- a/truffle/src/com.oracle.truffle.api.bytecode/snapshot.sigtest +++ b/truffle/src/com.oracle.truffle.api.bytecode/snapshot.sigtest @@ -71,6 +71,26 @@ meth public static com.oracle.truffle.api.bytecode.BytecodeEncodingException cre supr java.lang.RuntimeException hfds serialVersionUID +CLSS public final com.oracle.truffle.api.bytecode.BytecodeFrame +meth public com.oracle.truffle.api.bytecode.BytecodeFrame copy() +meth public com.oracle.truffle.api.bytecode.BytecodeLocation getLocation() +meth public com.oracle.truffle.api.bytecode.BytecodeNode getBytecodeNode() +meth public int getArgumentCount() +meth public int getBytecodeIndex() +meth public int getLocalCount() +meth public java.lang.Object getArgument(int) +meth public java.lang.Object getFrameDescriptorInfo() +meth public java.lang.Object getLocalValue(int) +meth public java.lang.Object[] getLocalNames() +meth public static com.oracle.truffle.api.bytecode.BytecodeFrame get(com.oracle.truffle.api.TruffleStackTraceElement) +meth public static com.oracle.truffle.api.bytecode.BytecodeFrame get(com.oracle.truffle.api.frame.FrameInstance,com.oracle.truffle.api.frame.FrameInstance$FrameAccess) +meth public static com.oracle.truffle.api.bytecode.BytecodeFrame getNonVirtual(com.oracle.truffle.api.TruffleStackTraceElement) +meth public static com.oracle.truffle.api.bytecode.BytecodeFrame getNonVirtual(com.oracle.truffle.api.frame.FrameInstance) +meth public void setArgument(int,java.lang.Object) +meth public void setLocalValue(int,java.lang.Object) +supr java.lang.Object +hfds bytecode,bytecodeIndex,frame + CLSS public abstract com.oracle.truffle.api.bytecode.BytecodeLabel cons public init(java.lang.Object) supr java.lang.Object @@ -106,6 +126,7 @@ cons protected init(java.lang.Object) meth protected abstract boolean isLocalClearedInternal(com.oracle.truffle.api.frame.Frame,int,int) meth protected abstract boolean validateBytecodeIndex(int) meth protected abstract com.oracle.truffle.api.bytecode.Instruction findInstruction(int) +meth protected abstract com.oracle.truffle.api.frame.Frame resolveFrameImpl(com.oracle.truffle.api.frame.FrameInstance,com.oracle.truffle.api.frame.FrameInstance$FrameAccess) meth protected abstract int findBytecodeIndex(com.oracle.truffle.api.frame.Frame,com.oracle.truffle.api.nodes.Node) meth protected abstract int findBytecodeIndex(com.oracle.truffle.api.frame.FrameInstance) meth protected abstract int translateBytecodeIndex(com.oracle.truffle.api.bytecode.BytecodeNode,int) @@ -116,6 +137,8 @@ meth protected abstract void clearLocalValueInternal(com.oracle.truffle.api.fram meth protected abstract void setLocalValueInternal(com.oracle.truffle.api.frame.Frame,int,int,java.lang.Object) meth protected boolean getLocalValueInternalBoolean(com.oracle.truffle.api.frame.Frame,int,int) throws com.oracle.truffle.api.nodes.UnexpectedResultException meth protected byte getLocalValueInternalByte(com.oracle.truffle.api.frame.Frame,int,int) throws com.oracle.truffle.api.nodes.UnexpectedResultException +meth protected com.oracle.truffle.api.frame.Frame resolveFrameImpl(com.oracle.truffle.api.TruffleStackTraceElement) +meth protected com.oracle.truffle.api.frame.Frame resolveNonVirtualFrameImpl(com.oracle.truffle.api.TruffleStackTraceElement) meth protected double getLocalValueInternalDouble(com.oracle.truffle.api.frame.Frame,int,int) throws com.oracle.truffle.api.nodes.UnexpectedResultException meth protected final com.oracle.truffle.api.bytecode.BytecodeLocation findLocation(int) meth protected final static java.lang.Object createDefaultStackTraceElement(com.oracle.truffle.api.TruffleStackTraceElement) @@ -143,6 +166,8 @@ meth public abstract java.util.List getSourceInformation() meth public abstract void setLocalValue(int,com.oracle.truffle.api.frame.Frame,int,java.lang.Object) meth public abstract void setUncachedThreshold(int) +meth public final com.oracle.truffle.api.bytecode.BytecodeFrame createCopiedFrame(int,com.oracle.truffle.api.frame.Frame) +meth public final com.oracle.truffle.api.bytecode.BytecodeFrame createMaterializedFrame(int,com.oracle.truffle.api.frame.MaterializedFrame) meth public final com.oracle.truffle.api.bytecode.BytecodeLocation getBytecodeLocation(com.oracle.truffle.api.frame.Frame,com.oracle.truffle.api.nodes.Node) meth public final com.oracle.truffle.api.bytecode.BytecodeLocation getBytecodeLocation(com.oracle.truffle.api.frame.FrameInstance) meth public final com.oracle.truffle.api.bytecode.BytecodeLocation getBytecodeLocation(int) @@ -332,6 +357,7 @@ CLSS public abstract interface !annotation com.oracle.truffle.api.bytecode.Gener intf java.lang.annotation.Annotation meth public abstract !hasdefault boolean additionalAssertions() meth public abstract !hasdefault boolean allowUnsafe() +meth public abstract !hasdefault boolean captureFramesForTrace() meth public abstract !hasdefault boolean enableBlockScoping() meth public abstract !hasdefault boolean enableBytecodeDebugListener() meth public abstract !hasdefault boolean enableInstructionTracing() @@ -758,7 +784,7 @@ meth public void printHistogram(java.io.PrintStream) meth public void reset() supr java.lang.Object hfds cache,counters,descriptor,filterClause,groupClauses,rootCounters -hcls Counters,LRUCache +hcls Counters,LastTraceCache CLSS public final static com.oracle.truffle.api.bytecode.debug.HistogramInstructionTracer$Builder outer com.oracle.truffle.api.bytecode.debug.HistogramInstructionTracer @@ -793,7 +819,7 @@ meth public void onInstructionEnter(com.oracle.truffle.api.bytecode.InstructionT meth public void reset() supr java.lang.Object hfds cache,executedInstructions,filter,out -hcls LRUCache +hcls LastTraceCache CLSS public final static com.oracle.truffle.api.bytecode.debug.PrintInstructionTracer$Builder outer com.oracle.truffle.api.bytecode.debug.PrintInstructionTracer