Interface InstrumentableNode

All Superinterfaces:
NodeInterface

public interface InstrumentableNode extends NodeInterface
Interface implemented by AST nodes that may be instrumentable: an AST location where Truffle instruments are permitted to listen to before and after using execution event listeners.

If a node is instrumentable depends on the return value of isInstrumentable(). All instrumentable nodes must also extend node. All other member methods of this interface are only allowed to be invoked if isInstrumentable() returns true .

Every instrumentable node is required to create a wrapper for this instrumentable node in createWrapper(ProbeNode). The instrumentation framework will, when needed during execution, replace the instrumentable node with a wrapper and delegate to the original node. After the replacement of an instrumentable node with a wrapper we refer to the original node as an instrumented node.

Wrappers can be generated automatically using an annotation processor by annotating the class with @GenerateWrapper. If an instrumentable node subclass has additional declared methods than its instrumentable base class that are used by other nodes, then a new wrapper should be generated or implemented for the subclass, otherwise the replacement of the wrapper will fail.

Instrumentable nodes may return true to indicate that they were tagged by tag. Tags are used by guest languages to indicate that a node is a member of a certain category of nodes. For example a debugger instrument might require a guest language to tag all nodes as statements that should be considered as such. See hasTag(Class) for further details on how to use tags.

Example minimal implementation of an instrumentable node:

@GenerateWrapper
abstract static class SimpleNode extends Node
                implements InstrumentableNode {

    public abstract Object execute(VirtualFrame frame);

    public boolean isInstrumentable() {
        return true;
    }

    public WrapperNode createWrapper(ProbeNode probe) {
        // ASTNodeWrapper is generated by @GenerateWrapper
        return new SimpleNodeWrapper(this, probe);
    }
}

Example for a typical implementation of an instrumentable node with support for source sections:

@GenerateWrapper
abstract static class RecommendedNode extends Node
                implements InstrumentableNode {

    private static final int NO_SOURCE = -1;

    private int sourceCharIndex = NO_SOURCE;
    private int sourceLength;

    public abstract Object execute(VirtualFrame frame);

    // invoked by the parser to set the source
    void setSourceSection(int charIndex, int length) {
        assert sourceCharIndex == NO_SOURCE : "source should only be set once";
        this.sourceCharIndex = charIndex;
        this.sourceLength = length;
    }

    public final boolean isInstrumentable() {
        // all AST nodes with source are instrumentable
        return sourceCharIndex != NO_SOURCE;
    }

    @Override
    @TruffleBoundary
    public final SourceSection getSourceSection() {
        if (sourceCharIndex == NO_SOURCE) {
            // AST node without source
            return null;
        }
        RootNode rootNode = getRootNode();
        if (rootNode == null) {
            // not yet adopted yet
            return null;
        }
        Source source = rootNode.getSourceSection().getSource();
        return source.createSection(sourceCharIndex, sourceLength);
    }

    public WrapperNode createWrapper(ProbeNode probe) {
        // ASTNodeWrapper is generated by @GenerateWrapper
        return new RecommendedNodeWrapper(this, probe);
    }
}

Since:
0.33
See Also:
  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Interface
    Description
    static interface 
    Nodes that the instrumentation framework inserts into guest language ASTs (between instrumentable guest language nodes and their parents) for the purpose of interposing on execution events and reporting them via the instrumentation framework.
  • Method Summary

    Modifier and Type
    Method
    Description
    Returns a new, never adopted, unshared wrapper node implementation for this instrumentable node.
    static Node
    Find the first instrumentable node on it's parent chain.
    default Node
    findNearestNodeAt(int line, int column, Set<Class<? extends Tag>> tags)
    Find the nearest node to the given source line and column position, according to the guest language control flow, that is tagged with some of the given tags.
    default Node
    findNearestNodeAt(int sourceCharIndex, Set<Class<? extends Tag>> tags)
    Find the nearest node to the given source character index according to the guest language control flow, that is tagged with some of the given tags.
    default Object
    Returns an interop capable object that contains all keys and values of attributes associated with this node.
    default boolean
    hasTag(Class<? extends Tag> tag)
    Returns true if this node should be considered tagged by a given tag else false.
    boolean
    Returns true if this node is instrumentable.
    materializeInstrumentableNodes(Set<Class<? extends Tag>> materializedTags)
    Removes optimizations performed in this AST node to restore the syntactic AST structure.
  • Method Details

    • isInstrumentable

      boolean isInstrumentable()
      Returns true if this node is instrumentable. Instrumentable nodes are points where instrumenters can attach execution events. The return values of instrumentable nodes must always be interop capable values.

      The implementation of this method must ensure that its result is stable after the parent root node was wrapped in a CallTarget using RootNode.getCallTarget(). The result is stable if the result of calling this method remains always the same.

      This method might be called in parallel from multiple threads even if the language is single threaded. The method may be invoked without a language context currently being active.

      Since:
      0.33
    • createWrapper

      Returns a new, never adopted, unshared wrapper node implementation for this instrumentable node. The returned wrapper implementation must extend the same type that implements InstrumentableNode.

      The instrumentation framework will, when needed during execution, replace the instrumentable node with a wrapper and delegate to the original node. After the replacement of an instrumentable node with a wrapper we refer to the original node as an instrumented node. Wrappers can be generated automatically using an annotation processor by annotating the class with @GenerateWrapper. Please note that if an instrumetnable node subclass has additional execute methods then a new wrapper must be generated or implemented. Otherwise the replacement of the instrumentable node with the wrapper will fail if the subtype is used as static type in nodes children.

      A wrapper forwards the following events concerning the delegate to the given probe for propagation through the instrumentation framework, e.g. to event listeners bound to this guest language program location:

      This method is always invoked on an interpreter thread. The method may be invoked without a language context currently being active.

      Parameters:
      probe - the probe node to be adopted and sent execution events by the wrapper
      Returns:
      a wrapper implementation
      Since:
      0.33
    • hasTag

      default boolean hasTag(Class<? extends Tag> tag)
      Returns true if this node should be considered tagged by a given tag else false. In order for a Truffle language to support a particular tag, the tag must also be marked as provided by the language.

      Tags are used by guest languages to indicate that a node is a member of a certain category of nodes. For example a debugger instrument might require a guest language to tag all nodes as statements that should be considered as such.

      The node implementor may decide how to implement tagging for nodes. The simplest way to implement tagging using Java types is by overriding the hasTag(Class) method. This example shows how to tag a node subclass and all its subclasses as statement:

      @GenerateWrapper
      abstract static class StatementNode extends SimpleNode
                      implements InstrumentableNode {
      
          @Override
          public final Object execute(VirtualFrame frame) {
              executeVoid(frame);
              return null;
          }
      
          public abstract void executeVoid(VirtualFrame frame);
      
          @Override
          public final WrapperNode createWrapper(ProbeNode probe) {
              return StatementNodeWrapper.create(this, probe);
          }
      
          public boolean hasTag(Class<? extends Tag> tag) {
              if (tag == StandardTags.StatementTag.class) {
                  return true;
              }
              return false;
          }
      }
      

      Often it is impossible to just rely on the node's Java type to implement tagging. This example shows how to use local state to implement tagging for a node.

      @GenerateWrapper
      static class HaltNode extends Node implements InstrumentableNode {
          private boolean isDebuggerHalt;
      
          public void setDebuggerHalt(boolean isDebuggerHalt) {
              this.isDebuggerHalt = isDebuggerHalt;
          }
      
          public Object execute(VirtualFrame frame) {
              // does nothing;
              return null;
          }
      
          public boolean isInstrumentable() {
              return true;
          }
      
          public boolean hasTag(Class<? extends Tag> tag) {
              if (tag == Debugger.HaltTag.class) {
                  return isDebuggerHalt;
              }
              return false;
          }
      
          public WrapperNode createWrapper(ProbeNode probe) {
              return new HaltNodeWrapper(this, probe);
          }
      
      }
      
      

      The implementation of hasTag method must ensure that its result is stable after the parent root node was wrapped in a CallTarget using RootNode.getCallTarget(). The result is stable if the result of calling this method for a particular tag remains always the same.

      This method might be called in parallel from multiple threads even if the language is single threaded. The method may be invoked without a language context currently being active.

      Parameters:
      tag - the class provided by the language
      Returns:
      true if the node should be considered tagged by a tag else false.
      Since:
      0.33
    • getNodeObject

      default Object getNodeObject()
      Returns an interop capable object that contains all keys and values of attributes associated with this node. The returned object must return true in response to the has members message. If null is returned then an empty tag object without any readable keys will be assumed. Multiple calls to getNodeObject() for a particular node may return the same or objects with different identity. The returned object must not support any write operation. The returned object must not support execution or instantiation and must not have a size.

      For performance reasons it is not recommended to eagerly collect all properties of the node object when getNodeObject() is invoked. Instead, the language should lazily compute them when they are read. If the node object contains dynamic properties, that change during the execution of the AST, then the node must return an updated value for each key when it is read repeatedly. In other words the node object must always represent the current state of this AST node. The implementer should not cache the node instance in the AST. The instrumentation framework will take care of caching node object instances when they are requested by tools.

      Compatibility: In addition to the expected keys by the tag specification, the language implementation may provide any set of additional keys and values. Tools might depend on these language specific tags and might break if keys or values are changed without notice.

      For a memory efficient implementation the language might make the instrumentable Node a TruffleObject and return this instance.

      This method might be called in parallel from multiple threads even if the language is single threaded. The method may be invoked without a language context currently being active. The AST lock is held while getNodeObject() object is invoked. There is no lock held when the object is read.

      Returns:
      the node object as TruffleObject or null if no node object properties are available for this instrumented node
      Since:
      0.33
    • materializeInstrumentableNodes

      default InstrumentableNode materializeInstrumentableNodes(Set<Class<? extends Tag>> materializedTags)
      Removes optimizations performed in this AST node to restore the syntactic AST structure. Guest languages may decide to group multiple nodes together into a single node. This is useful to reduce the memory consumed by the AST representation and it can also improve the execution performance when interpreting the AST. Performing such optimizations often modify the syntactic AST structure, leading to invalid execution events reported to the instrumentation framework. Implementing this method allows the instrumented node to restore the syntactic AST structure when needed. It provides a list of tags that were requested by all current execution event bindings to allow the language to do the materialization selectively for instrumentable nodes with certain tags only.

      The returned instrumentable nodes must return themselves when this method is called on them with the same tags. Materialized nodes should not be re-materialized again. Instrumentation relies on the stability of materialized nodes. Use Node.notifyInserted(Node) when you need to change the structure of instrumentable nodes.

      Node must return itself from this method when it has already seen all the materializedTags specified as an argument, i.e., not only if the set of tags is exactly the same as before, but also if the current set of tags is completely contained in the union of all the sets of tags specified in all the calls of this method that led to creation of this materialized node.

      If the node returns a new node from this method, the subtree rooted at the new node must be completely unadopted, i.e., all nodes it contains must not have existed in the original AST. Also, the new subtree must be completely materialized, so that no new materializations occur when the instrumentation framework instruments the new subtree during the current traversal.

      The AST lock is acquired while this method is invoked. Therefore it is not allowed to run guest language code while this method is invoked. This method might be called in parallel from multiple threads even if the language is single threaded. The method may be invoked without a language context currently being active. Language reference is always available.

      In the example below, we show how the IncrementNode with a ConstantNode child is optimized into a ConstantIncrementNode and how it can implement materializeSyntaxNodes to restore the syntactic structure of the AST:

      @GenerateWrapper
      abstract static class ExpressionNode extends Node
                      implements InstrumentableNode {
          abstract int execute(VirtualFrame frame);
      
          public boolean isInstrumentable() {
              return true;
          }
      
          public boolean hasTag(Class<? extends Tag> tag) {
              return tag == StandardTags.ExpressionTag.class;
          }
      
          public WrapperNode createWrapper(ProbeNode probe) {
              return new ExpressionNodeWrapper(this, probe);
          }
      }
      
      class ConstantNode extends ExpressionNode {
      
          private final int constant;
      
          ConstantNode(int constant) {
              this.constant = constant;
          }
      
          @Override
          int execute(VirtualFrame frame) {
              return constant;
          }
      
      }
      
      // node with constant folded operation
      class ConstantIncrementNode extends ExpressionNode {
          final int constantIncremented;
      
          ConstantIncrementNode(int constant) {
              this.constantIncremented = constant + 1;
          }
      
          // desguar to restore syntactic structure of the AST
          public InstrumentableNode materializeInstrumentableNodes(
                          Set<Class<? extends Tag>> tags) {
              if (tags.contains(StandardTags.ExpressionTag.class)) {
                  return new IncrementNode(
                                  new ConstantNode(constantIncremented - 1));
              }
              return this;
          }
      
          @Override
          int execute(VirtualFrame frame) {
              return constantIncremented;
          }
      
      }
      
      // node with full semantics of the node.
      class IncrementNode extends ExpressionNode {
          @Child ExpressionNode child;
      
          IncrementNode(ExpressionNode child) {
              this.child = child;
          }
      
          @Override
          int execute(VirtualFrame frame) {
              return child.execute(frame) + 1;
          }
      }
      
      Parameters:
      materializedTags - a set of tags that requested to be materialized
      Since:
      0.33
    • findNearestNodeAt

      default Node findNearestNodeAt(int sourceCharIndex, Set<Class<? extends Tag>> tags)
      Find the nearest node to the given source character index according to the guest language control flow, that is tagged with some of the given tags. The source character index is in this node's source. The nearest node will preferably be in the same block/function as the character index. This node acts as a context node - either a node containing the character index if such node exists, or node following the character index if exists, or node preceding the character index otherwise.

      Return an instrumentable node that is tagged with some of the tags and containing the character index, if such exists and there is not a more suitable sibling node inside the container source section. Return the next sibling tagged node otherwise, or the previous one when the next one does not exist.

      Use Case
      The current use-case of this method is a relocation of breakpoint position, for instance. When a user submits a breakpoint at the source character index, a nearest logical instrumentable node that has suitable tags needs to be found to move the breakpoint accordingly.

      Default Implementation
      This method has a default implementation, which assumes that the materialized Truffle Node hierarchy corresponds with the logical guest language AST structure. If this is not the case for a particular guest language, this method needs to be implemented, possibly with the help of language specific AST node classes.

      The default algorithm is following:

      1. If the character index is smaller than the start index of this node's source section, return the first tagged child of this node.
      2. If the character index is larger than the end index of this node's source section, return the last tagged child of this node.
      3. Otherwise, this node's source section contains the character index. Use following steps to find the nearest tagged node in this node's hierarchy:
        1. Traverse the node children in declaration order (AST breadth-first order). For every child do:
          1. When the child is not instrumentable, include its children into the traversal.
          2. When the child does not have a source section assigned, ignore it.
          3. When the sourceCharIndex is inside the child's source section, find if it's tagged with one of the tags (store as isTagged) and repeat recursively from 3.a. using this child as the node.
          4. When the child is above the character index, remember a sorted list of such children up to the lowest tagged child (store in higherNodes list).
          5. When the child is below the character index, remember a sorted list of such children down to the highest tagged child (store in lowerNodes list).
        2. If a tagged child node was found in 3.a with source section matching the sourceCharIndex, return it.
        3. Otherwise, we check the list of lower/higher nodes:
          1. Prefer the node after the character index.
          2. Traverse higherNodes in ascending order. When the node is tagged, return it, when not, repeat with that node from 3.a.
          3. If no tagged node was found, traverse lowerNodes in descending order. When the node is tagged, return it, when not, repeat with that node from 3.a.
          4. When nothing was found in the steps above, return null.
        4. If c. didn't provide a tagged node, apply this algorithm recursively to a parent of this node, if exists. If you encounter the nearest tagged parent node found in 3.a, return it. Otherwise, return a tagged child found in the steps above, if any.
      Parameters:
      sourceCharIndex - the 0-based character index in this node's source, to find the nearest tagged node from
      tags - a set of tags, the nearest node needs to be tagged with at least one tag from this set
      Returns:
      the nearest instrumentable node according to the execution flow and tagged with some of the tags, or null when none was found
      Since:
      0.33
      See Also:
    • findNearestNodeAt

      default Node findNearestNodeAt(int line, int column, Set<Class<? extends Tag>> tags)
      Find the nearest node to the given source line and column position, according to the guest language control flow, that is tagged with some of the given tags.

      Behaves in the same way as findNearestNodeAt(int, Set) but uses line/column as the position specification instead of a character index.

      Parameters:
      line - 1-based line number
      column - 1-based column number, or less than one when the column is unknown
      tags - a set of tags, the nearest node needs to be tagged with at least one tag from this set
      Returns:
      the nearest instrumentable node according to the execution flow and tagged with some of the tags, or null when none was found
      Since:
      23.0
      See Also:
    • findInstrumentableParent

      static Node findInstrumentableParent(Node node)
      Find the first instrumentable node on it's parent chain. If the provided node is instrumentable itself, it is returned. If not, the first parent node that is instrumentable is returned, if any.
      Parameters:
      node - a Node
      Returns:
      the first instrumentable node, or null when no instrumentable parent exists.
      Since:
      20.3