All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.truffle.api.debug.Breakpoint Maven / Gradle / Ivy

Go to download

Truffle is a multi-language framework for executing dynamic languages that achieves high performance when combined with Graal.

The newest version!
/*
 * Copyright (c) 2015, 2018, 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.debug;

import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import com.oracle.truffle.api.Assumption;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.EventBinding;
import com.oracle.truffle.api.instrumentation.EventContext;
import com.oracle.truffle.api.instrumentation.ExecuteSourceEvent;
import com.oracle.truffle.api.instrumentation.ExecuteSourceListener;
import com.oracle.truffle.api.instrumentation.ExecutionEventNode;
import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory;
import com.oracle.truffle.api.instrumentation.SourceFilter;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.nodes.ControlFlowException;
import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.ExecutableNode;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.nodes.SlowPathException;
import com.oracle.truffle.api.profiles.BranchProfile;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;

/**
 * A request that guest language program execution be suspended at specified locations on behalf of
 * a debugging client {@linkplain DebuggerSession session}.
 * 

*

Breakpoint lifetime

*

*

    *
  • A client of a {@link DebuggerSession} uses a {@link Builder} to create a new breakpoint, * choosing among multiple ways to specify the intended location. Examples include a specified * {@link #newBuilder(Source) source}, a specified {@link #newBuilder(URI) URI}, line ranges, or an * exact {@link #newBuilder(SourceSection) SourceSection}.
  • * *
  • A new breakpoint cannot affect execution until after it has been * {@linkplain DebuggerSession#install(Breakpoint) installed} by a session, which may be done only * once.
  • * *
  • A breakpoint that is both installed and {@linkplain Breakpoint#isEnabled() enabled} (true by * default) will suspend any guest language execution thread that arrives at a matching AST * location. The breakpoint (synchronously) {@linkplain SuspendedCallback calls back} to the * responsible session on the execution thread.
  • * *
  • A breakpoint may be enabled or disabled any number of times.
  • * *
  • A breakpoint that is no longer needed may be {@linkplain #dispose() disposed}. A disposed * breakpoint: *
      *
    • is disabled
    • *
    • is not installed in any session
    • *
    • can have no effect on program execution, and
    • *
    • must not be used again.
    • *
    *
  • * *
  • A session being {@linkplain DebuggerSession#close() closed} disposes all installed * breakpoints.
  • *
*

*

* Example usage: {@link com.oracle.truffle.api.debug.BreakpointSnippets#example()} * * @since 0.9 */ public class Breakpoint { /** * Specifies a breakpoint kind. Breakpoints with different kinds have different creation methods * and address different debugging needs. * * @since 1.0 */ public enum Kind { /** * Represents breakpoints submitted for a halt instruction in a guest language program. For * instance, in JavaScript this is debugger statement. Guest languages mark * such nodes with {@link DebuggerTags.AlwaysHalt}. A breakpoint of this kind is created by * {@link DebuggerSession} automatically. * * @since 1.0 */ HALT_INSTRUCTION, /** * Represents breakpoints submitted for a particular source code location. Use one of the * newBuilder methods to create a breakpoint of this kind. * * @since 1.0 */ SOURCE_LOCATION, /** * Represents exception breakpoints that are hit when an exception is thrown from a guest * language program. Use {@link #newExceptionBuilder(boolean, boolean)} to create a * breakpoint of this kind. * * @since 1.0 */ EXCEPTION; static final Kind[] VALUES = values(); } private static final Breakpoint BUILDER_INSTANCE = new Breakpoint(); private final SuspendAnchor suspendAnchor; private final BreakpointLocation locationKey; private final boolean oneShot; private final BreakpointExceptionFilter exceptionFilter; private final ResolveListener resolveListener; private volatile Debugger debugger; private final List sessions = new LinkedList<>(); private volatile Assumption sessionsUnchanged; private volatile boolean enabled; private volatile boolean resolved; private volatile int ignoreCount; private volatile boolean disposed; private volatile String condition; private volatile boolean global; private volatile GlobalBreakpoint roWrapper; /* We use long instead of int in the implementation to avoid not hitting again on overflows. */ private final AtomicLong hitCount = new AtomicLong(); private volatile Assumption conditionUnchanged; private volatile Assumption conditionExistsUnchanged; private volatile EventBinding breakpointBinding; private EventBinding sourceBinding; Breakpoint(BreakpointLocation key, SuspendAnchor suspendAnchor) { this(key, suspendAnchor, false, null, null); } private Breakpoint(BreakpointLocation key, SuspendAnchor suspendAnchor, boolean oneShot, BreakpointExceptionFilter exceptionFilter, ResolveListener resolveListener) { this.locationKey = key; this.suspendAnchor = suspendAnchor; this.oneShot = oneShot; this.exceptionFilter = exceptionFilter; this.resolveListener = resolveListener; this.enabled = true; } private Breakpoint() { this.locationKey = null; this.suspendAnchor = SuspendAnchor.BEFORE; this.oneShot = false; this.exceptionFilter = null; this.resolveListener = null; } /** * Returns the kind of this breakpoint. * * @since 1.0 */ public Kind getKind() { if (locationKey == null) { return Kind.HALT_INSTRUCTION; } else if (exceptionFilter == null) { return Kind.SOURCE_LOCATION; } else { return Kind.EXCEPTION; } } /** * @return whether this breakpoint is permanently unable to affect execution * @see #dispose() * * @since 0.17 */ public boolean isDisposed() { return disposed; } /** * @return whether this breakpoint is currently allowed to suspend execution (true by default) * @see #setEnabled(boolean) * * @since 0.9 */ public boolean isEnabled() { return enabled; } /** * Controls whether this breakpoint is currently allowed to suspend execution (true by default). * This can be changed arbitrarily until breakpoint is {@linkplain #dispose() disposed}. *

* When not {@link #isModifiable() modifiable}, {@link IllegalStateException} is thrown. * * @param enabled whether this breakpoint should be allowed to suspend execution * * @since 0.9 */ public synchronized void setEnabled(boolean enabled) { if (disposed) { // cannot enable disposed breakpoints return; } if (this.enabled != enabled) { if (!sessions.isEmpty()) { if (enabled) { install(); } else { uninstall(); } } this.enabled = enabled; } } /** * @return whether at least one source has been loaded that contains a match for this * breakpoint's location. * * @since 0.17 */ public boolean isResolved() { return resolved; } /** * Assigns to this breakpoint a boolean expression whose evaluation will determine whether the * breakpoint suspends execution (i.e. "hits"), {@code null} to remove any condition and always * suspend. *

* Breakpoints are by default unconditional. *

*

* Evaluation: expressions are parsed and evaluated in the lexical context of * the breakpoint's location. A conditional breakpoint that applies to multiple code locations * will be parsed and evaluated separately for each location. *

*

* Evaluation failure: when evaluation of a condition fails for any reason, * including the return of a non-boolean value: *

    *
  • execution suspends, as if evaluation had returned {@code true}, and
  • *
  • a message is logged that can be * {@linkplain SuspendedEvent#getBreakpointConditionException(Breakpoint) retrieved} while * execution is suspended.
  • *
* When not {@link #isModifiable() modifiable}, {@link IllegalStateException} is thrown. * * @param expression if non{@code -null}, a boolean expression, expressed in the guest language * of the breakpoint's location. * @see SuspendedEvent#getBreakpointConditionException(Breakpoint) * * @since 0.9 */ public synchronized void setCondition(String expression) { boolean existsChanged = (this.condition == null) != (expression == null); this.condition = expression; Assumption assumption = conditionUnchanged; if (assumption != null) { this.conditionUnchanged = null; assumption.invalidate(); } if (existsChanged) { assumption = conditionExistsUnchanged; if (assumption != null) { this.conditionExistsUnchanged = null; assumption.invalidate(); } } } /** * Returns the expression used to create the current breakpoint condition, null if no condition * set. * * @since 0.20 */ @SuppressFBWarnings("UG") public String getCondition() { return condition; } /** * Permanently prevents this breakpoint from affecting execution. When not * {@link #isModifiable() modifiable}, {@link IllegalStateException} is thrown. * * @since 0.9 */ public synchronized void dispose() { if (!disposed) { setEnabled(false); if (sourceBinding != null) { sourceBinding.dispose(); sourceBinding = null; } for (DebuggerSession session : sessions) { session.disposeBreakpoint(this); } if (debugger != null) { debugger.disposeBreakpoint(this); debugger = null; } disposed = true; } } /** * @return whether this breakpoint disables itself after suspending execution, i.e. on first hit * * @since 0.9 */ public boolean isOneShot() { return oneShot; } /** * @return the number of times breakpoint will be executed but not hit (i.e. suspend execution). * @see #setIgnoreCount(int) * * @since 0.9 */ public int getIgnoreCount() { return ignoreCount; } /** * Changes the number of times the breakpoint must be executed before it hits (i.e. suspends * execution). *

* When a breakpoint {@linkplain #setCondition(String) condition} evaluates to {@code false}: *

    *
  • execution is not suspended
  • *
  • it does not count as a hit
  • *
  • the remaining {@code ignoreCount} does not change.
  • *
* When not {@link #isModifiable() modifiable}, {@link IllegalStateException} is thrown. * * @param ignoreCount number of breakpoint activations to ignore before it hits * * @since 0.9 */ public void setIgnoreCount(int ignoreCount) { this.ignoreCount = ignoreCount; } /** * @return the number of times this breakpoint has suspended execution * * @since 0.9 */ public int getHitCount() { return (int) hitCount.get(); } /** * @return a description of this breakpoint's specified location * * @since 0.9 */ public String getLocationDescription() { return locationKey.toString(); } /** * Returns the suspended position within the guest language source location. * * @since 0.32 */ public SuspendAnchor getSuspendAnchor() { return suspendAnchor; } /** * Test whether this breakpoint can be modified. When false, methods that change * breakpoint state throw {@link IllegalStateException}. *

* Unmodifiable breakpoints are created from installed breakpoints as read-only copies to be * available to clients other than the one who installed the original breakpoint. * {@link Debugger#getBreakpoints()} returns unmodifiable breakpoints, for instance. * * @return whether this breakpoint can be modified. * @since 0.27 */ public boolean isModifiable() { return true; } /** * {@inheritDoc} * * @since 0.9 */ @Override public String toString() { return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); } DebuggerNode lookupNode(EventContext context) { if (!isEnabled()) { return null; } else { EventBinding binding = breakpointBinding; if (binding != null) { return (DebuggerNode) context.lookupExecutionEventNode(binding); } return null; } } private synchronized Assumption getConditionUnchanged() { if (conditionUnchanged == null) { conditionUnchanged = Truffle.getRuntime().createAssumption("Breakpoint condition unchanged."); } return conditionUnchanged; } private synchronized Assumption getConditionExistsUnchanged() { if (conditionExistsUnchanged == null) { conditionExistsUnchanged = Truffle.getRuntime().createAssumption("Breakpoint condition existence unchanged."); } return conditionExistsUnchanged; } synchronized void installGlobal(Debugger d) { if (disposed) { throw new IllegalArgumentException("Cannot install breakpoint, it is disposed already."); } if (this.debugger != null) { throw new IllegalStateException("Breakpoint is already installed in a Debugger instance."); } install(d); this.global = true; } private void install(Debugger d) { assert Thread.holdsLock(this); if (this.debugger != null && this.debugger != d) { throw new IllegalStateException("Breakpoint is already installed in a different Debugger instance."); } this.debugger = d; if (exceptionFilter != null) { exceptionFilter.setDebugger(d); } } synchronized boolean install(DebuggerSession d, boolean failOnError) { if (disposed) { if (failOnError) { throw new IllegalArgumentException("Cannot install breakpoint, it is disposed already."); } else { return false; } } if (this.sessions.contains(d)) { if (failOnError) { throw new IllegalStateException("Breakpoint is already installed in the session."); } else { return true; } } install(d.getDebugger()); this.sessions.add(d); sessionsAssumptionInvalidate(); if (enabled) { install(); } return true; } private void install() { assert Thread.holdsLock(this); SourceFilter filter; if (sourceBinding == null && (filter = locationKey.createSourceFilter()) != null) { final boolean[] sourceResolved = new boolean[]{false}; sourceBinding = debugger.getInstrumenter().attachExecuteSourceListener(filter, new ExecuteSourceListener() { @Override public void onExecute(ExecuteSourceEvent event) { if (sourceResolved[0]) { return; } sourceResolved[0] = true; synchronized (Breakpoint.this) { if (sourceBinding != null) { sourceBinding.dispose(); } } Source source = event.getSource(); SourceSection location = locationKey.adjustLocation(source, debugger.getEnv(), suspendAnchor); if (location != null) { resolveBreakpoint(location); } SourceSectionFilter locationFilter = locationKey.createLocationFilter(source, suspendAnchor); breakpointBinding = createBinding(locationFilter); } }, true); if (sourceResolved[0]) { sourceBinding.dispose(); } } else if (breakpointBinding == null && (sourceBinding == null || sourceBinding.isDisposed())) { // re-installing breakpoint resolved = true; SourceSectionFilter locationFilter = locationKey.createLocationFilter(null, suspendAnchor); breakpointBinding = createBinding(locationFilter); } } private EventBinding createBinding(SourceSectionFilter locationFilter) { EventBinding binding = debugger.getInstrumenter().attachExecutionEventFactory(locationFilter, new BreakpointNodeFactory()); synchronized (this) { for (DebuggerSession s : sessions) { s.allBindings.add(binding); } } return binding; } boolean isGlobal() { return global; } synchronized void sessionClosed(DebuggerSession d) { this.sessions.remove(d); sessionsAssumptionInvalidate(); if (this.sessions.isEmpty()) { uninstall(); } } private void sessionsAssumptionInvalidate() { assert Thread.holdsLock(this); Assumption assumption = sessionsUnchanged; if (assumption != null) { this.sessionsUnchanged = null; assumption.invalidate(); } } private void resolveBreakpoint(SourceSection resolvedLocation) { boolean notifyResolved = false; synchronized (this) { if (disposed) { // cannot resolve disposed breakpoint return; } if (!isResolved()) { notifyResolved = true; resolved = true; } } if (notifyResolved && resolveListener != null) { resolveListener.breakpointResolved(Breakpoint.this, resolvedLocation); } } private void uninstall() { assert Thread.holdsLock(this); EventBinding binding = breakpointBinding; breakpointBinding = null; for (DebuggerSession s : sessions) { s.allBindings.remove(binding); } if (binding != null) { binding.dispose(); } resolved = false; } /** * Returns true if it should appear in the breakpoints list. * * @throws BreakpointConditionFailure */ boolean notifyIndirectHit(DebuggerNode source, DebuggerNode node, MaterializedFrame frame, DebugException exception) throws BreakpointConditionFailure { if (!isEnabled()) { return false; } assert node.getBreakpoint() == this; if (source != node) { // We're testing a different breakpoint at the same location if (!((AbstractBreakpointNode) node).testCondition(frame)) { return false; } if (exceptionFilter != null && exception != null) { Throwable throwable = exception.getRawException(); assert throwable != null; BreakpointExceptionFilter.Match matched = exceptionFilter.matchException(node, throwable); if (!matched.isMatched) { return false; } } if (this.hitCount.incrementAndGet() <= ignoreCount) { // breakpoint hit was ignored return false; } } if (isOneShot()) { setEnabled(false); } return true; } @TruffleBoundary private void doBreak(DebuggerNode source, DebuggerSession[] breakInSessions, MaterializedFrame frame, boolean onEnter, Object result, Throwable exception, BreakpointConditionFailure failure) { DebugException de; if (exception != null) { de = new DebugException(debugger, exception, null, source, false, null); } else { de = null; } doBreak(source, breakInSessions, frame, onEnter, result, de, failure); } @TruffleBoundary private void doBreak(DebuggerNode source, DebuggerSession[] breakInSessions, MaterializedFrame frame, boolean onEnter, Object result, DebugException exception, BreakpointConditionFailure failure) { if (!isEnabled()) { // make sure we do not cause break events if we got disabled already // the instrumentation framework will make sure that this is not happening if the // binding was disposed. return; } if (this.hitCount.incrementAndGet() <= ignoreCount) { // breakpoint hit was ignored return; } SuspendAnchor anchor = onEnter ? SuspendAnchor.BEFORE : SuspendAnchor.AFTER; for (DebuggerSession session : breakInSessions) { if (session.isBreakpointsActive(getKind())) { session.notifyCallback(source, frame, anchor, null, result, exception, failure); } } } Breakpoint getROWrapper() { assert global; // wrappers are for global breakpoints only GlobalBreakpoint wrapper = roWrapper; if (wrapper == null) { synchronized (this) { wrapper = roWrapper; if (wrapper == null) { roWrapper = wrapper = new GlobalBreakpoint(this); } } } return wrapper; } /** * Creates a new breakpoint builder based on a URI location. * * @param sourceUri a URI to specify breakpoint location * * @since 0.17 */ public static Builder newBuilder(URI sourceUri) { return BUILDER_INSTANCE.new Builder(sourceUri); } /** * Creates a new breakpoint builder based on a {@link Source}. * * @param source a {@link Source} to specify breakpoint location * * @since 0.17 */ public static Builder newBuilder(Source source) { return BUILDER_INSTANCE.new Builder(source); } /** * Creates a new breakpoint builder based on the textual region of a guest language source * element. * * @param sourceSection a specification for guest language source element * * @since 0.17 */ public static Builder newBuilder(SourceSection sourceSection) { return BUILDER_INSTANCE.new Builder(sourceSection); } /** * Creates a new exception breakpoint builder. The exception breakpoint can be set to intercept * caught or uncaught exceptions, or both. At least one argument needs to be true. The builder * creates {@link Breakpoint breakpoint} of {@link Kind#EXCEPTION EXCEPTION} kind. * * @param caught true to intercept exceptions that are caught by guest language * code. * @param uncaught true to intercept exceptions that are not caught by guest * language code. * @since 1.0 */ public static ExceptionBuilder newExceptionBuilder(boolean caught, boolean uncaught) { if (!(caught || uncaught)) { throw new IllegalArgumentException("At least one of 'caught' or 'uncaught' needs to be true."); } return BUILDER_INSTANCE.new ExceptionBuilder(caught, uncaught); } /** * Builder implementation for a new {@link Breakpoint breakpoint}. * * @see Breakpoint#newBuilder(Source) * @see Breakpoint#newBuilder(URI) * @see Breakpoint#newBuilder(SourceSection) * * @since 0.17 */ public final class Builder { private final Object key; private int line = -1; private SuspendAnchor anchor = SuspendAnchor.BEFORE; private int column = -1; private ResolveListener resolveListener; private int ignoreCount; private boolean oneShot; private SourceSection sourceSection; private SourceElement[] sourceElements; private Builder(Object key) { Objects.requireNonNull(key); this.key = key; } private Builder(SourceSection key) { this(key.getSource()); Objects.requireNonNull(key); sourceSection = key; } /** * Specifies the breakpoint's line number. * * Can only be invoked once per builder. Cannot be used together with * {@link Breakpoint#newBuilder(SourceSection)}. * * @param line 1-based line number * @throws IllegalStateException if {@code line < 1} * * @since 0.17 */ public Builder lineIs(@SuppressWarnings("hiding") int line) { if (line <= 0) { throw new IllegalArgumentException("Line argument must be > 0."); } if (this.line != -1) { throw new IllegalStateException("LineIs can only be called once per breakpoint builder."); } if (sourceSection != null) { throw new IllegalArgumentException("LineIs cannot be used with source section based breakpoint. "); } this.line = line; return this; } /** * Specify the breakpoint suspension anchor within the guest language source location. By * default, the breakpoint suspends {@link SuspendAnchor#BEFORE before} the source location. * * @param anchor the breakpoint suspension anchor * @since 0.32 */ public Builder suspendAnchor(@SuppressWarnings("hiding") SuspendAnchor anchor) { this.anchor = anchor; return this; } /** * Specifies the breakpoint's column number. * * Can only be invoked once per builder. Cannot be used together with * {@link Breakpoint#newBuilder(SourceSection)}. A line needs to be specified before a * column can be set. * * @param column 1-based column number * @throws IllegalStateException if {@code column < 1} * * @since 0.33 */ public Builder columnIs(@SuppressWarnings("hiding") int column) { if (column <= 0) { throw new IllegalArgumentException("Column argument must be > 0."); } if (this.line == -1) { throw new IllegalStateException("ColumnIs can only be called after a line is set."); } this.column = column; return this; } /** * Set a resolve listener. The listener is called when the breakpoint is resolved at the * target location. A breakpoint is not resolved till the target source section is loaded. * The target resolved location may differ from the specified {@link #lineIs(int) line} and * {@link #columnIs(int) column}. * * @since 0.33 */ public Builder resolveListener(@SuppressWarnings("hiding") ResolveListener resolveListener) { Objects.requireNonNull(resolveListener); if (this.resolveListener != null) { throw new IllegalStateException("ResolveListener can only be set once per breakpoint builder."); } this.resolveListener = resolveListener; return this; } /** * Specifies the number of times a breakpoint is ignored until it hits (i.e. suspends * execution}. * * @see Breakpoint#setIgnoreCount(int) * * @since 0.17 */ public Builder ignoreCount(@SuppressWarnings("hiding") int ignoreCount) { if (ignoreCount < 0) { throw new IllegalArgumentException("IgnoreCount argument must be >= 0."); } this.ignoreCount = ignoreCount; return this; } /** * Specifies that the breakpoint will {@linkplain Breakpoint#setEnabled(boolean) disable} * itself after suspending execution, i.e. on first hit. *

* Disabled one-shot breakpoints can be {@linkplain Breakpoint#setEnabled(boolean) * re-enabled}. * * @since 0.17 */ public Builder oneShot() { this.oneShot = true; return this; } /** * Specifies which source elements will this breakpoint adhere to. When not specified, * breakpoint adhere to {@link SourceElement#STATEMENT} elements. Can only be invoked once * per builder. * * @param sourceElements a non-empty list of source elements * @since 0.33 */ public Builder sourceElements(@SuppressWarnings("hiding") SourceElement... sourceElements) { if (this.sourceElements != null) { throw new IllegalStateException("Step source elements can only be set once per the builder."); } if (sourceElements.length == 0) { throw new IllegalArgumentException("At least one source element needs to be provided."); } this.sourceElements = sourceElements; return this; } /** * @return a new breakpoint instance of {@link Kind#SOURCE_LOCATION SOURCE_LOCATION} kind. * * @since 0.17 */ public Breakpoint build() { if (sourceElements == null) { sourceElements = new SourceElement[]{SourceElement.STATEMENT}; } BreakpointLocation location; if (sourceSection != null) { location = BreakpointLocation.create(key, sourceElements, sourceSection); } else { location = BreakpointLocation.create(key, sourceElements, line, column); } Breakpoint breakpoint = new Breakpoint(location, anchor, oneShot, null, resolveListener); breakpoint.setIgnoreCount(ignoreCount); return breakpoint; } } /** * Builder implementation for a new {@link Breakpoint breakpoint} of {@link Kind#EXCEPTION * EXCEPTION} kind. * * @see Breakpoint#newExceptionBuilder(boolean, boolean) * @since 1.0 */ public final class ExceptionBuilder { private final boolean caught; private final boolean uncaught; private SuspensionFilter suspensionFilter; private SourceElement[] sourceElements; ExceptionBuilder(boolean caught, boolean uncaught) { this.caught = caught; this.uncaught = uncaught; } /** * A filter to limit source locations which intercept exceptions. Only the source locations * matching the filter will report thrown exceptions. * * @since 1.0 */ public ExceptionBuilder suspensionFilter(SuspensionFilter filter) { this.suspensionFilter = filter; return this; } /** * Specifies which source elements will this breakpoint adhere to. When not specified, * breakpoint adhere to {@link SourceElement#STATEMENT} elements. Can only be invoked once * per builder. * * @param sourceElements a non-empty list of source elements * @since 1.0 */ public ExceptionBuilder sourceElements(@SuppressWarnings("hiding") SourceElement... sourceElements) { if (this.sourceElements != null) { throw new IllegalStateException("Step source elements can only be set once per the builder."); } if (sourceElements.length == 0) { throw new IllegalArgumentException("At least one source element needs to be provided."); } this.sourceElements = sourceElements.clone(); return this; } /** * @return a new breakpoint instance of {@link Kind#EXCEPTION EXCEPTION} kind. * * @since 1.0 */ public Breakpoint build() { if (sourceElements == null) { sourceElements = new SourceElement[]{SourceElement.STATEMENT}; } BreakpointLocation location = BreakpointLocation.create(sourceElements, suspensionFilter); BreakpointExceptionFilter efilter = new BreakpointExceptionFilter(caught, uncaught); return new Breakpoint(location, SuspendAnchor.AFTER, false, efilter, null); } } /** * This listener is called when a breakpoint is resolved at the target location. The breakpoint * is not resolved till the target source section is loaded. The target resolved location may * differ from the specified breakpoint location. * * @since 0.33 */ public interface ResolveListener { /** * Notify about a breakpoint resolved at the specified location. * * @param breakpoint The resolved breakpoint * @param section The resolved location * @since 0.33 */ void breakpointResolved(Breakpoint breakpoint, SourceSection section); } private class BreakpointNodeFactory implements ExecutionEventNodeFactory { @Override public ExecutionEventNode create(EventContext context) { if (!isResolved()) { resolveBreakpoint(context.getInstrumentedSourceSection()); } if (exceptionFilter != null) { return new BreakpointAfterNodeException(Breakpoint.this, context); } switch (suspendAnchor) { case BEFORE: return new BreakpointBeforeNode(Breakpoint.this, context); case AFTER: return new BreakpointAfterNode(Breakpoint.this, context); default: throw new IllegalStateException("Unknown suspend anchor: " + suspendAnchor); } } } private static class BreakpointBeforeNode extends AbstractBreakpointNode { BreakpointBeforeNode(Breakpoint breakpoint, EventContext context) { super(breakpoint, context); } @Override Set getSuspendAnchors() { return DebuggerSession.ANCHOR_SET_BEFORE; } @Override boolean isActiveAt(SuspendAnchor anchor) { return SuspendAnchor.BEFORE == anchor; } @Override protected void onEnter(VirtualFrame frame) { onNode(frame, true, null, null); } } private static class BreakpointAfterNode extends AbstractBreakpointNode { BreakpointAfterNode(Breakpoint breakpoint, EventContext context) { super(breakpoint, context); } @Override Set getSuspendAnchors() { return DebuggerSession.ANCHOR_SET_AFTER; } @Override boolean isActiveAt(SuspendAnchor anchor) { return SuspendAnchor.AFTER == anchor; } @Override protected void onReturnValue(VirtualFrame frame, Object result) { onNode(frame, false, result, null); } @Override protected void onReturnExceptional(VirtualFrame frame, Throwable exception) { if (!(exception instanceof ControlFlowException || exception instanceof ThreadDeath)) { onNode(frame, false, null, exception); } } } private static class BreakpointAfterNodeException extends AbstractBreakpointNode { BreakpointAfterNodeException(Breakpoint breakpoint, EventContext context) { super(breakpoint, context); } @Override Set getSuspendAnchors() { return DebuggerSession.ANCHOR_SET_AFTER; } @Override boolean isActiveAt(SuspendAnchor anchor) { return SuspendAnchor.AFTER == anchor; } @Override public void onEnter(VirtualFrame frame) { getBreakpoint().exceptionFilter.resetReportedException(); } @Override public void onReturnValue(VirtualFrame frame, Object result) { getBreakpoint().exceptionFilter.resetReportedException(); } @Override @ExplodeLoop protected void onReturnExceptional(VirtualFrame frame, Throwable exception) { if (!(exception instanceof ControlFlowException || exception instanceof ThreadDeath)) { DebuggerSession[] debuggerSessions = getSessions(); boolean active = false; List nonDuplicateSessions = null; for (DebuggerSession session : debuggerSessions) { if (consumeIsDuplicate(session)) { if (nonDuplicateSessions == null) { if (debuggerSessions.length == 1) { // This node is marked as duplicate in the only session return; } } nonDuplicateSessions = removeDuplicateSession(debuggerSessions, session, nonDuplicateSessions); } else if (session.isBreakpointsActive(getBreakpoint().getKind())) { active = true; } } if (!active) { return; } if (nonDuplicateSessions != null) { if (nonDuplicateSessions.isEmpty()) { return; } debuggerSessions = toSessionsArray(nonDuplicateSessions); } BreakpointExceptionFilter.Match matched = getBreakpoint().exceptionFilter.matchException(this, exception); if (matched.isMatched) { BreakpointConditionFailure conditionError = null; try { if (!testCondition(frame)) { return; } } catch (BreakpointConditionFailure e) { conditionError = e; } breakBranch.enter(); doBreak(frame.materialize(), debuggerSessions, conditionError, exception, matched); } } } @TruffleBoundary void doBreak(MaterializedFrame frame, DebuggerSession[] debuggerSessions, BreakpointConditionFailure conditionError, Throwable exception, BreakpointExceptionFilter.Match matched) { Node throwLocation = getContext().getInstrumentedNode(); DebugException de = new DebugException(getBreakpoint().debugger, exception, null, throwLocation, matched.isCatchNodeComputed, matched.catchLocation); getBreakpoint().doBreak(this, debuggerSessions, frame, false, null, de, conditionError); } } private abstract static class AbstractBreakpointNode extends DebuggerNode { private final Breakpoint breakpoint; protected final BranchProfile breakBranch = BranchProfile.create(); @Child private ConditionalBreakNode breakCondition; @CompilationFinal private Assumption conditionExistsUnchanged; @CompilationFinal(dimensions = 1) private DebuggerSession[] sessions; @CompilationFinal private Assumption sessionsUnchanged; AbstractBreakpointNode(Breakpoint breakpoint, EventContext context) { super(context); this.breakpoint = breakpoint; initializeSessions(); this.conditionExistsUnchanged = breakpoint.getConditionExistsUnchanged(); if (breakpoint.condition != null) { this.breakCondition = new ConditionalBreakNode(context, breakpoint); } } private void initializeSessions() { CompilerAsserts.neverPartOfCompilation(); synchronized (breakpoint) { this.sessions = breakpoint.sessions.toArray(new DebuggerSession[]{}); sessionsUnchanged = Truffle.getRuntime().createAssumption("Breakpoint sessions unchanged."); breakpoint.sessionsUnchanged = sessionsUnchanged; } } @Override boolean isStepNode() { return false; } @Override Breakpoint getBreakpoint() { return breakpoint; } @Override EventBinding getBinding() { return breakpoint.breakpointBinding; } @ExplodeLoop protected final void onNode(VirtualFrame frame, boolean onEnter, Object result, Throwable exception) { if (!sessionsUnchanged.isValid()) { CompilerDirectives.transferToInterpreterAndInvalidate(); initializeSessions(); } DebuggerSession[] debuggerSessions = sessions; boolean active = false; List sessionsWithUniqueNodes = null; for (DebuggerSession session : debuggerSessions) { if (consumeIsDuplicate(session)) { if (sessionsWithUniqueNodes == null) { if (debuggerSessions.length == 1) { // This node is marked as duplicate in the only session that's there. return; } } sessionsWithUniqueNodes = removeDuplicateSession(debuggerSessions, session, sessionsWithUniqueNodes); } else if (session.isBreakpointsActive(breakpoint.getKind())) { active = true; } } if (!active) { return; } if (sessionsWithUniqueNodes != null) { if (sessionsWithUniqueNodes.isEmpty()) { return; } debuggerSessions = toSessionsArray(sessionsWithUniqueNodes); } if (!conditionExistsUnchanged.isValid()) { CompilerDirectives.transferToInterpreterAndInvalidate(); if (breakpoint.condition != null) { this.breakCondition = insert(new ConditionalBreakNode(context, breakpoint)); notifyInserted(this.breakCondition); } else { this.breakCondition = null; } conditionExistsUnchanged = breakpoint.getConditionExistsUnchanged(); } BreakpointConditionFailure conditionError = null; try { if (!testCondition(frame)) { return; } } catch (BreakpointConditionFailure e) { conditionError = e; } breakBranch.enter(); breakpoint.doBreak(this, debuggerSessions, frame.materialize(), onEnter, result, exception, conditionError); } final DebuggerSession[] getSessions() { if (!sessionsUnchanged.isValid()) { CompilerDirectives.transferToInterpreterAndInvalidate(); initializeSessions(); } return sessions; } boolean testCondition(VirtualFrame frame) throws BreakpointConditionFailure { if (!conditionExistsUnchanged.isValid()) { CompilerDirectives.transferToInterpreterAndInvalidate(); if (breakpoint.condition != null) { this.breakCondition = insert(new ConditionalBreakNode(context, breakpoint)); notifyInserted(this.breakCondition); } else { this.breakCondition = null; } conditionExistsUnchanged = breakpoint.getConditionExistsUnchanged(); } if (breakCondition != null) { try { return breakCondition.executeBreakCondition(frame, sessions); } catch (Throwable e) { CompilerDirectives.transferToInterpreter(); throw new BreakpointConditionFailure(breakpoint, e); } } return true; } } @TruffleBoundary private static List removeDuplicateSession(DebuggerSession[] sessions, DebuggerSession session, List nonDuplicateSessionsList) { List nonDuplicateSessions = nonDuplicateSessionsList; if (nonDuplicateSessions == null) { nonDuplicateSessions = new ArrayList<>(sessions.length); for (DebuggerSession s : sessions) { if (s != session) { nonDuplicateSessions.add(s); } } } else { nonDuplicateSessions.remove(session); } return nonDuplicateSessions; } @TruffleBoundary private static DebuggerSession[] toSessionsArray(List sessions) { return sessions.toArray(new DebuggerSession[sessions.size()]); } static final class BreakpointConditionFailure extends SlowPathException { private static final long serialVersionUID = 1L; private final Breakpoint breakpoint; BreakpointConditionFailure(Breakpoint breakpoint, Throwable cause) { super(cause); this.breakpoint = breakpoint; } public Breakpoint getBreakpoint() { return breakpoint; } public Throwable getConditionFailure() { return getCause(); } } private static class ConditionalBreakNode extends Node { private static final Object[] EMPTY_ARRAY = new Object[0]; private final EventContext context; private final Breakpoint breakpoint; @Child private SetThreadSuspensionEnabledNode suspensionEnabledNode = SetThreadSuspensionEnabledNodeGen.create(); @Child private DirectCallNode conditionCallNode; @Child private ExecutableNode conditionSnippet; @CompilationFinal private Assumption conditionUnchanged; ConditionalBreakNode(EventContext context, Breakpoint breakpoint) { this.context = context; this.breakpoint = breakpoint; this.conditionUnchanged = breakpoint.getConditionUnchanged(); } boolean executeBreakCondition(VirtualFrame frame, DebuggerSession[] sessions) { if ((conditionSnippet == null && conditionCallNode == null) || !conditionUnchanged.isValid()) { CompilerDirectives.transferToInterpreterAndInvalidate(); initializeConditional(frame.materialize()); } Object result; try { suspensionEnabledNode.execute(false, sessions); if (conditionSnippet != null) { result = conditionSnippet.execute(frame); } else { result = conditionCallNode.call(EMPTY_ARRAY); } } finally { suspensionEnabledNode.execute(true, sessions); } if (!(result instanceof Boolean)) { CompilerDirectives.transferToInterpreter(); throw new IllegalArgumentException("Unsupported return type " + result + " in condition."); } return (Boolean) result; } private void initializeConditional(MaterializedFrame frame) { Node instrumentedNode = context.getInstrumentedNode(); final RootNode rootNode = instrumentedNode.getRootNode(); if (rootNode == null) { throw new IllegalStateException("Probe was disconnected from the AST."); } Source instrumentedSource = context.getInstrumentedSourceSection().getSource(); Source conditionSource; synchronized (breakpoint) { conditionSource = Source.newBuilder(instrumentedSource.getLanguage(), breakpoint.condition, "breakpoint condition").mimeType(instrumentedSource.getMimeType()).build(); if (conditionSource == null) { throw new IllegalStateException("Condition is not resolved " + rootNode); } conditionUnchanged = breakpoint.getConditionUnchanged(); } ExecutableNode snippet = breakpoint.debugger.getEnv().parseInline(conditionSource, instrumentedNode, frame); if (snippet != null) { conditionSnippet = insert(snippet); notifyInserted(snippet); } else { CallTarget callTarget = Debugger.ACCESSOR.parse(conditionSource, instrumentedNode, new String[0]); conditionCallNode = insert(Truffle.getRuntime().createDirectCallNode(callTarget)); } } } /** * A read-only wrapper over "global" breakpoint installed on {@link Debugger}. Instances of this * wrapper are for public access to global breakpoints. */ @SuppressWarnings("sync-override") static final class GlobalBreakpoint extends Breakpoint { private final Breakpoint delegate; GlobalBreakpoint(Breakpoint delegate) { this.delegate = delegate; } @Override public void dispose() { fail(); } @Override public void setCondition(String expression) { fail(); } @Override public void setEnabled(boolean enabled) { fail(); } @Override public void setIgnoreCount(int ignoreCount) { fail(); } private static void fail() { throw new IllegalStateException("Unmodifiable breakpoint."); } @Override public boolean isModifiable() { return false; } @Override public String getCondition() { return delegate.getCondition(); } @Override public int getHitCount() { return delegate.getHitCount(); } @Override public int getIgnoreCount() { return delegate.getIgnoreCount(); } @Override public String getLocationDescription() { return delegate.getLocationDescription(); } @Override public SuspendAnchor getSuspendAnchor() { return delegate.getSuspendAnchor(); } @Override public boolean isDisposed() { return delegate.isDisposed(); } @Override public boolean isEnabled() { return delegate.isEnabled(); } @Override public boolean isOneShot() { return delegate.isOneShot(); } @Override public boolean isResolved() { return delegate.isResolved(); } } } class BreakpointSnippets { @SuppressFBWarnings("") public void example() { SuspendedCallback suspendedCallback = new SuspendedCallback() { public void onSuspend(SuspendedEvent event) { } }; Source someCode = Source.newBuilder("", "", "").build(); TruffleInstrument.Env instrumentEnvironment = null; // @formatter:off // BEGIN: BreakpointSnippets.example try (DebuggerSession session = Debugger.find(instrumentEnvironment). startSession(suspendedCallback)) { // install breakpoint in someCode at line 3. session.install(Breakpoint.newBuilder(someCode). lineIs(3).build()); // install breakpoint for a URI at line 3 session.install(Breakpoint.newBuilder(someCode.getURI()). lineIs(3).build()); } // END: BreakpointSnippets.example // @formatter:on } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy