view truffle/com.oracle.truffle.api/src/com/oracle/truffle/api/instrument/Instrumenter.java @ 22352:7d9b7365b675

Adding Map<String,Object> parameter to the attach(Eval*Listener) method to allow passing parameter values that the evaluated Source can reference.
author Jaroslav Tulach <jaroslav.tulach@oracle.com>
date Wed, 04 Nov 2015 16:54:36 +0100
parents 260e3cdf11ec
children
line wrap: on
line source

/*
 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.oracle.truffle.api.instrument;

import java.io.IOException;
import java.io.PrintStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.impl.Accessor;
import com.oracle.truffle.api.instrument.ProbeInstrument.EvalInstrument;
import com.oracle.truffle.api.instrument.TagInstrument.AfterTagInstrument;
import com.oracle.truffle.api.instrument.TagInstrument.BeforeTagInstrument;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import java.util.Map;

/**
 * Client access to instrumentation services in a Truffle execution environment.
 * <p>
 * Services include:
 * <ul>
 * <li>A collection of {@linkplain Probe Probes}, each of which is {@linkplain #probe(Node) created}
 * by clients in permanent association with a particular {@linkplain SourceSection source code
 * location} in an AST executing in this environment. The Probe keeps tracks of all <em>clones</em>
 * of the AST and ensures that any instrumentation <em>attached</em> to the Probe is put into effect
 * in each AST clone at the {@link Node} that corresponds to the Probe's source code location.</li>
 * <p>
 * <li>A collection of {@linkplain ProbeListener listeners} that have registered to be notified
 * whenever a new {@link Probe} is created in this environment and whenever a new {@link SyntaxTag}
 * (or simply "tag") is newly added to an existing {@link Probe} in this environment.</li>
 * <p>
 * <li>The ability to {@linkplain #findProbesTaggedAs(SyntaxTag) enumerate} all existing
 * {@linkplain Probe Probes} in this environment, optionally filtered to include only those to which
 * a specific {@linkplain SyntaxTag tag} has been added.</li>
 * <p>
 * <li>The ability to <em>attach</em> a client-provided <em>event listener</em> to an existing
 * {@link Probe} in this environment. The listener subsequently receives notification of execution
 * events at the {@linkplain Node Nodes} corresponding to the Probe's {@linkplain SourceSection
 * source code location}. The <em>attachment</em> also produces a {@link ProbeInstrument} that
 * represents the binding, and which can be used to {@linkplain Instrument#dispose() detach} the
 * listener from the Probe and stop event notification. A listener can be attached to any number of
 * Probes, each time producing a new {@linkplain ProbeInstrument} that represents the binding.</li>
 * <p>
 * <li>The ability to <em>attach</em> a client-provided <em>event listener</em> to a specific
 * {@linkplain SyntaxTag tag} for all {@linkplain Probe Probes} in the environment. A maximum of
 * <em>one</em> listener may be attached to receive notification of "<em>before</em>" execution
 * events (i.e. the flow of execution is just about to enter a {@link Node}), and a maximum of
 * <em>one</em> listener may be attached to receive notification of "<em>after</em>" execution
 * events. The <em>attachment</em> also produces a {@link TagInstrument} that represents the
 * binding, and which can be used to {@linkplain Instrument#dispose() detach} the listener from the
 * Probes and stop event notification. This mechanism is designed for much lower runtime overhead
 * than other ways to accomplish the same thing, e.g. by attaching one listener individually to
 * every Probe with the desired tag.</li>
 * <p>
 * <li>A collection of {@linkplain Tool Tools}, possibly client-provided, that can be
 * {@linkplain #install(Tool) installed} for data collection, possibly providing their own services
 * with the resulting information.</li>
 * </ul>
 */
public final class Instrumenter {

    private static final boolean TRACE = false;
    private static final String TRACE_PREFIX = "Instrumenter: ";
    private static final PrintStream OUT = System.out;

    private static void trace(String format, Object... args) {
        if (TRACE) {
            OUT.println(TRACE_PREFIX + String.format(format, args));
        }
    }

    private enum ToolState {

        /** Not yet installed, inert. */
        UNINSTALLED,

        /** Installed, collecting data. */
        ENABLED,

        /** Installed, not collecting data. */
        DISABLED,

        /** Was installed, but now removed, inactive, and no longer usable. */
        DISPOSED;
    }

    /**
     * {@linkplain Instrumenter Instrumentation}-based collectors of data during Guest Language
     * program execution.
     * <p>
     * Tools share a common <em>life cycle</em>:
     * <ul>
     * <li>A newly created tool is "UNINSTALLED"; it does nothing until
     * {@linkplain Instrumenter#install(Tool) installed} .</li>
     * <li>An installed tool becomes "ENABLED" and immediately begins attaching
     * {@linkplain ProbeInstrument instrumentation} to ASTs and collecting execution data.</li>
     * <li>A tool may only be installed once.</li>
     * <li>It is possible to install multiple instances of a tool, possibly (but not necessarily)
     * configured differently with respect to what data is being collected.</li>
     * <li>Once installed, a tool can be {@linkplain #setEnabled(boolean) "ENABLED" and "DISABLED"}
     * arbitrarily.</li>
     * <li>A disabled tool:
     * <ul>
     * <li>Collects no data;</li>
     * <li>Retains existing AST instrumentation;</li>
     * <li>Continues to instrument newly created ASTs; and</li>
     * <li>Retains previously collected data.</li>
     * </ul>
     * </li>
     * <li>An installed tool may be {@linkplain #reset() reset} at any time, which leaves the tool
     * installed but with all previously collected data removed.</li>
     * <li>A {@linkplain #dispose() disposed} tool removes all instrumentation (but not
     * {@linkplain Probe probes}) and becomes permanently disabled; previously collected data
     * persists.</li>
     * </ul>
     * <p>
     * Tool-specific methods that access data collected by the tool should:
     * <ul>
     * <li>Return modification-safe representations of the data; and</li>
     * <li>Not change the state of the data.</li>
     * </ul>
     */
    public abstract static class Tool {
        // TODO (mlvdv) still thinking about the most appropriate name for this class of tools

        private ToolState toolState = ToolState.UNINSTALLED;

        private Instrumenter instrumenter;

        protected Tool() {
        }

        final void install(Instrumenter inst) {
            checkUninstalled();
            this.instrumenter = inst;

            if (internalInstall()) {
                toolState = ToolState.ENABLED;
            }
            instrumenter.tools.add(this);
        }

        /**
         * @return whether the tool is currently collecting data.
         */
        public final boolean isEnabled() {
            return toolState == ToolState.ENABLED;
        }

        /**
         * Switches tool state between <em>enabled</em> (collecting data) and <em>disabled</em> (not
         * collecting data, but keeping data already collected).
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void setEnabled(boolean isEnabled) {
            checkInstalled();
            internalSetEnabled(isEnabled);
            toolState = isEnabled ? ToolState.ENABLED : ToolState.DISABLED;
        }

        /**
         * Clears any data already collected, but otherwise does not change the state of the tool.
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void reset() {
            checkInstalled();
            internalReset();
        }

        /**
         * Makes the tool permanently <em>disabled</em>, removes instrumentation, but keeps data
         * already collected.
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void dispose() {
            checkInstalled();
            internalDispose();
            toolState = ToolState.DISPOSED;
            instrumenter.tools.remove(this);
        }

        /**
         * @return whether the installation succeeded.
         */
        protected abstract boolean internalInstall();

        /**
         * No subclass action required.
         *
         * @param isEnabled
         */
        protected void internalSetEnabled(boolean isEnabled) {
        }

        protected abstract void internalReset();

        protected abstract void internalDispose();

        protected final Instrumenter getInstrumenter() {
            return instrumenter;
        }

        /**
         * Ensure that the tool is currently installed.
         *
         * @throws IllegalStateException
         */
        private void checkInstalled() throws IllegalStateException {
            if (toolState == ToolState.UNINSTALLED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " not yet installed");
            }
            if (toolState == ToolState.DISPOSED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " has been disposed");
            }
        }

        /**
         * Ensure that the tool has not yet been installed.
         *
         * @throws IllegalStateException
         */
        private void checkUninstalled() {
            if (toolState != ToolState.UNINSTALLED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " has already been installed");
            }
        }
    }

    private final Object vm;

    /** Tools that have been created, but not yet disposed. */
    Set<Tool> tools = new HashSet<>();

    private final Set<ASTProber> astProbers = Collections.synchronizedSet(new LinkedHashSet<ASTProber>());

    private final List<ProbeListener> probeListeners = new ArrayList<>();

    /**
     * All Probes that have been created.
     */
    private final List<WeakReference<Probe>> probes = new ArrayList<>();

    /**
     * A global instrument that triggers notification just before executing any Node that is Probed
     * with a matching tag.
     */
    @CompilationFinal private BeforeTagInstrument beforeTagInstrument = null;

    /**
     * A global instrument that triggers notification just after executing any Node that is Probed
     * with a matching tag.
     */
    @CompilationFinal private AfterTagInstrument afterTagInstrument = null;

    Instrumenter(Object vm) {
        this.vm = vm;
    }

    /**
     * Prepares an AST node for {@linkplain ProbeInstrument instrumentation}, where the node is
     * presumed to be part of a well-formed Truffle AST that has not yet been executed.
     * <p>
     * <em>Probing</em> a node is idempotent:
     * <ul>
     * <li>If the node has not been Probed, modifies the AST by first inserting a
     * {@linkplain #createWrapperNode(Node) wrapper node} between the node and its parent and then
     * returning the newly created Probe associated with the wrapper.</li>
     * <li>If the node has been Probed, returns the Probe associated with its existing wrapper.</li>
     * <li>No more than one {@link Probe} may be associated with a node, so a wrapper may not wrap
     * another wrapper.</li>
     * </ul>
     * It is a runtime error to attempt Probing an AST node with no parent.
     *
     * @return a (possibly newly created) {@link Probe} associated with this node.
     * @throws ProbeException (unchecked) when a Probe cannot be created, leaving the AST unchanged
     */
    @SuppressWarnings("rawtypes")
    public Probe probe(Node node) {

        final Node parent = node.getParent();

        if (node instanceof WrapperNode) {
            throw new ProbeException(ProbeFailure.Reason.WRAPPER_NODE, null, node, null);
        }

        if (parent == null) {
            throw new ProbeException(ProbeFailure.Reason.NO_PARENT, null, node, null);
        }

        if (parent instanceof WrapperNode) {
            final WrapperNode wrapper = (WrapperNode) parent;
            if (TRACE) {
                final Probe probe = wrapper.getProbe();
                final SourceSection sourceSection = wrapper.getChild().getSourceSection();
                final String location = sourceSection == null ? "<unknown>" : sourceSection.getShortDescription();
                trace("PROBE FOUND %s %s %s", "Probe@", location, probe.getTagsDescription());
            }
            return wrapper.getProbe();
        }

        if (!ACCESSOR.isInstrumentable(vm, node)) {
            throw new ProbeException(ProbeFailure.Reason.NOT_INSTRUMENTABLE, parent, node, null);
        }

        // Create a new wrapper/Probe with this node as its child.
        final WrapperNode wrapper = createWrapperNode(node);

        if (wrapper == null || !(wrapper instanceof Node)) {
            throw new ProbeException(ProbeFailure.Reason.NO_WRAPPER, parent, node, wrapper);
        }

        final Node wrapperNode = (Node) wrapper;

        if (!node.isSafelyReplaceableBy(wrapperNode)) {
            throw new ProbeException(ProbeFailure.Reason.WRAPPER_TYPE, parent, node, wrapper);
        }

        final SourceSection sourceSection = wrapper.getChild().getSourceSection();
        final ProbeNode probeNode = new ProbeNode();
        Class<? extends TruffleLanguage> l = ACCESSOR.findLanguage(wrapper.getChild().getRootNode());
        final Probe probe = new Probe(this, l, probeNode, sourceSection);
        probes.add(new WeakReference<>(probe));
        probeNode.probe = probe;  // package private access
        wrapper.insertEventHandlerNode(probeNode);
        node.replace(wrapperNode);
        if (TRACE) {
            final String location = sourceSection == null ? "<unknown>" : sourceSection.getShortDescription();
            trace("PROBED %s %s %s", "Probe@", location, probe.getTagsDescription());
        }
        for (ProbeListener listener : probeListeners) {
            listener.newProbeInserted(probe);
        }
        return probe;
    }

    /**
     * Adds a {@link ProbeListener} to receive events.
     */
    public void addProbeListener(ProbeListener listener) {
        assert listener != null;
        probeListeners.add(listener);
    }

    /**
     * Removes a {@link ProbeListener}. Ignored if listener not found.
     */
    public void removeProbeListener(ProbeListener listener) {
        probeListeners.remove(listener);
    }

    /**
     * Returns all {@link Probe}s holding a particular {@link SyntaxTag}, or the whole collection of
     * probes if the specified tag is {@code null}.
     *
     * @return A collection of probes containing the given tag.
     */
    public Collection<Probe> findProbesTaggedAs(SyntaxTag tag) {
        final List<Probe> taggedProbes = new ArrayList<>();
        for (WeakReference<Probe> ref : probes) {
            Probe probe = ref.get();
            if (probe != null) {
                if (tag == null || probe.isTaggedAs(tag)) {
                    taggedProbes.add(ref.get());
                }
            }
        }
        return taggedProbes;
    }

    /**
     * Enables instrumentation at selected nodes in all subsequently constructed ASTs. Ignored if
     * the argument is already registered, runtime error if argument is {@code null}.
     */
    public void registerASTProber(ASTProber prober) {
        if (prober == null) {
            throw new IllegalArgumentException("Register non-null ASTProbers");
        }
        astProbers.add(prober);
    }

    public void unregisterASTProber(ASTProber prober) {
        astProbers.remove(prober);
    }

    /**
     * <em>Attaches</em> a {@link SimpleInstrumentListener listener} to a {@link Probe}, creating a
     * <em>binding</em> called an {@link ProbeInstrument}. Until the Instrument is
     * {@linkplain ProbeInstrument#dispose() disposed}, it routes synchronous notification of
     * {@linkplain EventHandlerNode execution events} taking place at the Probe's AST location to
     * the listener.
     *
     * @param probe source of AST execution events, non-null
     * @param listener receiver of execution events
     * @param instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     */
    public ProbeInstrument attach(Probe probe, SimpleInstrumentListener listener, String instrumentInfo) {
        assert probe.getInstrumenter() == this;
        final ProbeInstrument instrument = new ProbeInstrument.SimpleInstrument(listener, instrumentInfo);
        probe.attach(instrument);
        return instrument;
    }

    /**
     * <em>Attaches</em> a {@link StandardInstrumentListener listener} to a {@link Probe}, creating
     * a <em>binding</em> called an {@link ProbeInstrument}. Until the Instrument is
     * {@linkplain ProbeInstrument#dispose() disposed}, it routes synchronous notification of
     * {@linkplain EventHandlerNode execution events} taking place at the Probe's AST location to
     * the listener.
     *
     * @param probe source of AST execution events, non-null
     * @param listener receiver of execution events
     * @param instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     */
    public ProbeInstrument attach(Probe probe, StandardInstrumentListener listener, String instrumentInfo) {
        assert probe.getInstrumenter() == this;
        final ProbeInstrument instrument = new ProbeInstrument.StandardInstrument(listener, instrumentInfo);
        probe.attach(instrument);
        return instrument;
    }

    /**
     * <em>Attaches</em> a fragment of source text that is to be evaluated just before execution
     * enters the location of a {@link Probe}, creating a <em>binding</em> called an
     * {@link ProbeInstrument}. The outcome of the evaluation is reported to an optional
     * {@link EvalInstrumentListener listener}, but the outcome does not affect the flow of guest
     * language execution, even if the evaluation produces an exception.
     * <p>
     * The source text is assumed to be expressed in the language identified by its associated
     * {@linkplain Source#getMimeType() MIME type}, if specified, otherwise by the language
     * associated with the AST location associated with the {@link Probe}.
     * <p>
     * The source text is parsed in the lexical context of the AST location associated with the
     * {@link Probe}.
     * <p>
     * The source text executes subject to full Truffle optimization.
     *
     * @param probe source of AST execution events, non-null
     * @param languageClass the language in which the source text is to be executed
     * @param source the source code to be evaluated, non-null and non-empty
     * @param listener optional client callback for results/failure notification
     * @param instrumentInfo instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     * @deprecated
     */
    @Deprecated
    @SuppressWarnings("rawtypes")
    public ProbeInstrument attach(Probe probe, Class<? extends TruffleLanguage> languageClass, Source source, EvalInstrumentListener listener, String instrumentInfo) {
        return attach(probe, languageClass, source, listener, instrumentInfo, new String[0], new Object[0]);
    }

    /**
     * <em>Attaches</em> a fragment of source text that is to be evaluated just before execution
     * enters the location of a {@link Probe}, creating a <em>binding</em> called an
     * {@link ProbeInstrument}. The outcome of the evaluation is reported to an optional
     * {@link EvalInstrumentListener listener}, but the outcome does not affect the flow of guest
     * language execution, even if the evaluation produces an exception.
     * <p>
     * The source text is assumed to be expressed in the language identified by its associated
     * {@linkplain Source#getMimeType() MIME type}, if specified, otherwise by the language
     * associated with the AST location associated with the {@link Probe}.
     * <p>
     * The source text is parsed in the lexical context of the AST location associated with the
     * {@link Probe}.
     * <p>
     * The source text executes subject to full Truffle optimization.
     *
     * @param probe source of AST execution events, non-null
     * @param source the source code to be evaluated, non-null and non-empty, preferably with
     *            {@link Source#withMimeType(java.lang.String) specified mime type} that determines
     *            the {@link TruffleLanguage} to use when processing the source
     * @param listener optional client callback for results/failure notification
     * @param instrumentInfo instrumentInfo optional documentation about the Instrument
     * @param parameters keys are the parameter names to pass to
     *            {@link TruffleLanguage#parse(com.oracle.truffle.api.source.Source, com.oracle.truffle.api.nodes.Node, java.lang.String...)
     *            parse} method; values will be passed to
     *            {@link CallTarget#call(java.lang.Object...)} returned from the <code>parse</code>
     *            method; the value can be <code>null</code>
     * @return a handle for access to the binding
     */
    public ProbeInstrument attach(Probe probe, Source source, EvalInstrumentListener listener, String instrumentInfo, Map<String, Object> parameters) {
        final int size = parameters == null ? 0 : parameters.size();
        String[] names = new String[size];
        Object[] params = new Object[size];
        if (parameters != null) {
            int index = 0;
            for (Map.Entry<String, Object> entry : parameters.entrySet()) {
                names[index] = entry.getKey();
                params[index] = entry.getValue();
                index++;
            }
        }
        return attach(probe, null, source, listener, instrumentInfo, names, params);
    }

    @SuppressWarnings("rawtypes")
    private ProbeInstrument attach(Probe probe, Class<? extends TruffleLanguage> languageClass, Source source, EvalInstrumentListener listener, String instrumentInfo, String[] argumentNames,
                    Object[] parameters) {
        assert probe.getInstrumenter() == this;
        Class<? extends TruffleLanguage> foundLanguageClass = null;
        if (languageClass == null) {
            if (source.getMimeType() == null) {
                foundLanguageClass = ACCESSOR.findLanguage(probe);
            }
        } else {
            foundLanguageClass = languageClass;
        }
        final EvalInstrument instrument = new EvalInstrument(foundLanguageClass, source, listener, instrumentInfo, argumentNames, parameters);
        probe.attach(instrument);
        return instrument;
    }

    // TODO (mlvdv) allow multiple <em>before</em> instruments without performance hit?
    /**
     * Sets the current "<em>before</em>" TagInstrument; there can be no more than one in effect.
     * <ul>
     * <li>The Instrument triggers a callback just <strong><em>before</em></strong> execution
     * reaches <strong><em>any</em></strong> {@link Probe} (either existing or subsequently created)
     * with the specified {@link SyntaxTag}.</li>
     * <li>Calling {@link TagInstrument#dispose()} removes the instrument.</li>
     * </ul>
     *
     * @param tag identifies the nodes to be instrumented
     * @param listener receiver of <em>before</em> execution events
     * @param instrumentInfo optional, mainly for debugging.
     * @return a newly created, active Instrument
     * @throws IllegalStateException if called when a <em>before</em> Instrument is active.
     */
    public TagInstrument attach(SyntaxTag tag, StandardBeforeInstrumentListener listener, String instrumentInfo) {
        if (beforeTagInstrument != null) {
            throw new IllegalStateException("Only one 'before' TagInstrument at a time");
        }
        this.beforeTagInstrument = new TagInstrument.BeforeTagInstrument(this, tag, listener, instrumentInfo);
        notifyTagInstrumentChange();
        return beforeTagInstrument;
    }

    // TODO (mlvdv) allow multiple <em>after</em> instruments without performance hit?
    /**
     * Sets the current "<em>after</em>" TagInstrument; there can be no more than one in effect.
     * <ul>
     * <li>The Instrument triggers a callback just <strong><em>after</em></strong> execution reaches
     * <strong><em>any</em></strong> {@link Probe} (either existing or subsequently created) with
     * the specified {@link SyntaxTag}.</li>
     * <li>Calling {@link TagInstrument#dispose()} removes the instrument.</li>
     * </ul>
     *
     * @param tag identifies the nodes to be instrumented
     * @param listener receiver of <em>after</em> execution events
     * @param instrumentInfo optional, mainly for debugging.
     * @return a newly created, active Instrument
     * @throws IllegalStateException if called when a <em>after</em> Instrument is active.
     */
    public TagInstrument attach(SyntaxTag tag, StandardAfterInstrumentListener listener, String instrumentInfo) {
        if (afterTagInstrument != null) {
            throw new IllegalStateException("Only one 'afater' TagInstrument at a time");
        }
        this.afterTagInstrument = new TagInstrument.AfterTagInstrument(this, tag, listener, instrumentInfo);
        notifyTagInstrumentChange();
        return afterTagInstrument;
    }

    /**
     * Connects the tool to some part of the Truffle runtime, and enable data collection to start.
     *
     * @return the tool
     * @throws IllegalStateException if the tool has previously been installed or has been disposed.
     */
    public Tool install(Tool tool) {
        tool.install(this);
        return tool;
    }

    @SuppressWarnings("unused")
    void executionStarted(Source s) {
    }

    void executionEnded() {
    }

    WrapperNode createWrapperNode(Node node) {
        return ACCESSOR.createWrapperNode(vm, node);
    }

    void tagAdded(Probe probe, SyntaxTag tag, Object tagValue) {
        for (ProbeListener listener : probeListeners) {
            listener.probeTaggedAs(probe, tag, tagValue);
        }
    }

    BeforeTagInstrument getBeforeTagInstrument() {
        return beforeTagInstrument;
    }

    AfterTagInstrument getAfterTagInstrument() {
        return afterTagInstrument;
    }

    void disposeBeforeTagInstrument() {
        beforeTagInstrument = null;
        notifyTagInstrumentChange();
    }

    void disposeAfterTagInstrument() {
        afterTagInstrument = null;
        notifyTagInstrumentChange();
    }

    private void notifyTagInstrumentChange() {
        for (WeakReference<Probe> ref : probes) {
            final Probe probe = ref.get();
            if (probe != null) {
                probe.notifyTagInstrumentsChanged();
            }
        }
    }

    /**
     * Enables instrumentation in a newly created AST by applying all registered instances of
     * {@link ASTProber}.
     */
    private void probeAST(RootNode rootNode) {
        if (!astProbers.isEmpty()) {

            String name = "<?>";
            final SourceSection sourceSection = rootNode.getSourceSection();
            if (sourceSection != null) {
                final Source source = sourceSection.getSource();
                if (source != null) {
                    name = source.getShortName();
                } else {
                    name = sourceSection.getShortDescription();
                }
            }
            trace("START %s", name);
            for (ProbeListener listener : probeListeners) {
                listener.startASTProbing(rootNode);
            }
            for (ASTProber prober : astProbers) {
                prober.probeAST(this, rootNode);
            }
            for (ProbeListener listener : probeListeners) {
                listener.endASTProbing(rootNode);
            }
            trace("FINISHED %s", name);
        }
    }

    static final class AccessorInstrument extends Accessor {

        @Override
        protected Instrumenter createInstrumenter(Object vm) {
            return new Instrumenter(vm);
        }

        @Override
        protected boolean isInstrumentable(Object vm, Node node) {
            return super.isInstrumentable(vm, node);
        }

        @Override
        protected WrapperNode createWrapperNode(Object vm, Node node) {
            return super.createWrapperNode(vm, node);
        }

        @SuppressWarnings("rawtypes")
        @Override
        protected Class<? extends TruffleLanguage> findLanguage(RootNode n) {
            return super.findLanguage(n);
        }

        @SuppressWarnings("rawtypes")
        @Override
        protected Class<? extends TruffleLanguage> findLanguage(Probe probe) {
            return probe.getLanguage();
        }

        @SuppressWarnings("rawtypes")
        @Override
        protected CallTarget parse(Class<? extends TruffleLanguage> languageClass, Source code, Node context, String... argumentNames) throws IOException {
            return super.parse(languageClass, code, context, argumentNames);
        }

        @Override
        protected void probeAST(RootNode rootNode) {
            // Normally null vm argument; can be reflectively set for testing
            Instrumenter instrumenter = super.getInstrumenter(testVM);
            if (instrumenter != null) {
                instrumenter.probeAST(rootNode);
            }
        }
    }

    static final AccessorInstrument ACCESSOR = new AccessorInstrument();

    // Normally null; set for testing where the Accessor hasn't been fully initialized
    private static Object testVM = null;

}