public interface InstrumentableNode extends NodeInterface
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 InstrumentableNode.isInstrumentable()
. All
instrumentable nodes must also extend node
. All other member methods of this
interface are only allowed to be invoked if InstrumentableNode.isInstrumentable()
returns true
.
Every instrumentable node is required to create a wrapper for this instrumentable node in
InstrumentableNode.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 InstrumentableNode.hasTag(Class)
for further details on how to use tags.
Example minimal implementation of an instrumentable node:
@GenerateWrapper
abstract static class SimpleNode extendsNode
implementsInstrumentableNode
{ public abstractObject
execute(VirtualFrame
frame); public boolean isInstrumentable() { return true; } publicInstrumentableNode.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 extendsNode
implementsInstrumentableNode
{ private static final int NO_SOURCE = -1; private int sourceCharIndex = NO_SOURCE; private int sourceLength; public abstractObject
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
@CompilerDirectives.TruffleBoundary
public finalSourceSection
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); } publicInstrumentableNode.WrapperNode
createWrapper(ProbeNode
probe) { // ASTNodeWrapper is generated by @GenerateWrapper return new RecommendedNodeWrapper(this, probe); } }
Modifier and Type | Interface and Description |
---|---|
static interface |
InstrumentableNode.WrapperNode
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. |
Modifier and Type | Method and Description |
---|---|
InstrumentableNode.WrapperNode |
createWrapper(ProbeNode probe)
Returns a new, never adopted, unshared
wrapper node implementation for
this instrumentable node. |
static Node |
findInstrumentableParent(Node 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 |
getNodeObject()
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 |
isInstrumentable()
Returns
true if this node is instrumentable. |
default InstrumentableNode |
materializeInstrumentableNodes(Set<Class<? extends Tag>> materializedTags)
Removes optimizations performed in this AST node to restore the syntactic AST structure.
|
boolean isInstrumentable()
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.
InstrumentableNode.WrapperNode createWrapper(ProbeNode probe)
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:
null
) value;This method is always invoked on an interpreter thread. The method may be invoked without a language context currently being active.
probe
- the probe node
to be adopted and sent execution events by the
wrapperwrapper
implementationdefault boolean hasTag(Class<? extends Tag> tag)
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 InstrumentableNode.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 implementsInstrumentableNode
{ @Override
public finalObject
execute(VirtualFrame
frame) { executeVoid(frame); return null; } public abstract void executeVoid(VirtualFrame
frame); @Override
public finalInstrumentableNode.WrapperNode
createWrapper(ProbeNode
probe) { return StatementNodeWrapper.create(this, probe); } public boolean hasTag(Class
<? extendsTag
> 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 extendsNode
implementsInstrumentableNode
{ private boolean isDebuggerHalt; public void setDebuggerHalt(boolean isDebuggerHalt) { this.isDebuggerHalt = isDebuggerHalt; } publicObject
execute(VirtualFrame
frame) { // does nothing; return null; } public boolean isInstrumentable() { return true; } public boolean hasTag(Class
<? extendsTag
> tag) { if (tag ==Debugger
.HaltTag.class) { return isDebuggerHalt; } return false; } publicInstrumentableNode.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.
default Object getNodeObject()
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 InstrumentableNode.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 InstrumentableNode.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 InstrumentableNode.getNodeObject()
object is invoked.
There is no lock held when the object is read.
null
if no node object properties
are available for this instrumented nodedefault InstrumentableNode materializeInstrumentableNodes(Set<Class<? extends Tag>> materializedTags)
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 extendsNode
implementsInstrumentableNode
{ abstract int execute(VirtualFrame
frame); public boolean isInstrumentable() { return true; } public boolean hasTag(Class
<? extendsTag
> tag) { return tag ==StandardTags
.ExpressionTag.class; } publicInstrumentableNode.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 publicInstrumentableNode
materializeInstrumentableNodes(Set
<Class
<? extendsTag
>> 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 { @Node.Child
ExpressionNode child; IncrementNode(ExpressionNode child) { this.child = child; } @Override
int execute(VirtualFrame
frame) { return child.execute(frame) + 1; } }
materializedTags
- a set of tags that requested to be materializeddefault Node findNearestNodeAt(int sourceCharIndex, Set<Class<? extends Tag>> tags)
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:
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.higherNodes
list).lowerNodes
list).sourceCharIndex
, return it.higherNodes
in ascending order. When the node is tagged, return it,
when not, repeat with that node from 3.a.lowerNodes
in descending order. When
the node is tagged, return it, when not, repeat with that node from 3.a.null
.sourceCharIndex
- the 0-based character index in this node's source, to find the nearest
tagged node fromtags
- a set of tags, the nearest node needs to be tagged with at least one tag from
this setnull
when none was foundInstrumentableNode.findNearestNodeAt(int, int, Set)
default Node findNearestNodeAt(int line, int column, Set<Class<? extends Tag>> tags)
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 InstrumentableNode.findNearestNodeAt(int, Set)
but uses line/column as the
position specification instead of a character index.
line
- 1-based line numbercolumn
- 1-based column number, or less than one when the column is unknowntags
- a set of tags, the nearest node needs to be tagged with at least one tag from
this setnull
when none was foundInstrumentableNode.findNearestNodeAt(int, Set)
static Node findInstrumentableParent(Node node)
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.node
- a Nodenull
when no instrumentable parent
exists.