oracle/graal · error · GraalError

%s must not kill %s

Error message

%s must not kill %s

What it means

When a node's usage count drops to zero, Graph delivers a ZERO_USAGES NodeEvent to registered NodeEventListeners. After invoking the listener's usagesDroppedToZero callback, the graph guarantees the node must still be alive; if the callback itself killed (removed) the node, this GraalError('%s must not kill %s') is thrown naming the listener and the killed node. It is a contract violation by the listener, not a graph corruption bug.

Source

Thrown at compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/graph/Graph.java:774

         * This method dispatches the event to user-defined triggers. The methods that change the
         * graph (typically in Graph and Node) must call this method to dispatch the event.
         *
         * @param e an event
         * @param node the node related to {@code e}
         */
        final void event(NodeEvent e, Node node) {
            switch (e) {
                case CONTROL_FLOW_CHANGED:
                    controlFlowChanged(node);
                    break;
                case INPUT_CHANGED:
                    inputChanged(node);
                    break;
                case ZERO_USAGES:
                    GraalError.guarantee(node.isAlive(), "must be alive");
                    usagesDroppedToZero(node);
                    if (!node.isAlive()) {
                        throw new GraalError("%s must not kill %s", this, node);
                    }
                    break;
                case NODE_ADDED:
                    nodeAdded(node);
                    break;
                case NODE_REMOVED:
                    nodeRemoved(node);
                    break;
                case BEFORE_DECODING_FIELDS:
                    beforeDecodingFields(node);
                    break;
                case AFTER_DECODING_FIELDS:
                    afterDecodingFields(node);
                    break;
            }
            changed(e, node);
        }

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Do not delete the node inside usagesDroppedToZero; only observe it. Defer any deletion to after the event (e.g. collect the node in a list and process it when the event scope closes).
  2. If you must rewire, replace usages of the node rather than deleting the node itself while inside the callback.
  3. Check whether you actually need ZERO_USAGES handling at all — NodeEventListener.usagesDroppedToZero has a default no-op implementation; override it only for genuine observation.

Example fix

// before
graph.addNodeEventListener(new NodeEventListener() {
    @Override
    public void usagesDroppedToZero(Node node) {
        node.safeDelete(); // GraalError: listener must not kill node
    }
});

// after
List<Node> dead = new ArrayList<>();
graph.addNodeEventListener(new NodeEventListener() {
    @Override
    public void usagesDroppedToZero(Node node) {
        dead.add(node); // observe only; delete later
    }
});
// ... after event scope:
// dead.forEach(Node::safeDelete);
Defensive patterns

Strategy: validation

Validate before calling

// contract check inside a listener
class ObservingListener implements NodeEventListener {
    private final List<Node> zeroUsage = new ArrayList<>();
    @Override public void usagesDroppedToZero(Node n) {
        // observe only — never mutate/delete here
        zeroUsage.add(n);
    }
    // process zeroUsage after the event scope ends
}

static boolean listenerSafe(NodeEventListener l, Node n) {
    l.event(NodeEvent.ZERO_USAGES, n);
    return n.isAlive(); // false means the listener violated the contract
}

Prevention

When it happens

Trigger: Implementing a NodeEventListener (e.g. via Graph.addNodeEventListener / NodeEventListener for incremental verification, tracking, or custom cleanup) whose usagesDroppedToZero override removes or replaces the node — for example calling node.replaceAndDelete(), node.safeDelete(), or rewriting inputs in a way that deletes the just-orphaned node inside the callback.

Common situations: Custom instrumentation/verification passes that watch for dead nodes and eagerly delete them; porting listeners written against older Graal where the contract was laxer; debugging utilities that 'clean up' zero-usage nodes inline instead of deferring.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/a5d26b0b4ca49b78. Report an issue: GitHub.