/*
 * Copyright (c) 2013, 2022, 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.svm.hosted.phases;

import org.graalvm.compiler.core.common.type.AbstractObjectStamp;
import org.graalvm.compiler.core.common.type.ObjectStamp;
import org.graalvm.compiler.core.common.type.Stamp;
import org.graalvm.compiler.core.common.type.StampFactory;
import org.graalvm.compiler.core.common.type.TypeReference;
import org.graalvm.compiler.graph.Node;
import org.graalvm.compiler.nodes.FixedGuardNode;
import org.graalvm.compiler.nodes.FixedWithNextNode;
import org.graalvm.compiler.nodes.InvokeWithExceptionNode;
import org.graalvm.compiler.nodes.LogicConstantNode;
import org.graalvm.compiler.nodes.NodeView;
import org.graalvm.compiler.nodes.ParameterNode;
import org.graalvm.compiler.nodes.PhiNode;
import org.graalvm.compiler.nodes.PiNode;
import org.graalvm.compiler.nodes.StructuredGraph;
import org.graalvm.compiler.nodes.ValueNode;
import org.graalvm.compiler.nodes.java.InstanceOfNode;
import org.graalvm.compiler.nodes.java.LoadFieldNode;
import org.graalvm.compiler.nodes.spi.LimitedValueProxy;
import org.graalvm.compiler.phases.Phase;

import com.oracle.svm.core.SubstrateOptions;
import com.oracle.svm.hosted.meta.HostedField;
import com.oracle.svm.hosted.meta.HostedType;

import jdk.vm.ci.meta.DeoptimizationAction;
import jdk.vm.ci.meta.DeoptimizationReason;
import jdk.vm.ci.meta.JavaKind;
import jdk.vm.ci.meta.JavaTypeProfile;
import jdk.vm.ci.meta.JavaTypeProfile.ProfiledType;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaType;
import jdk.vm.ci.meta.TriState;

/**
 * Strengthens the stamp of nodes based on the static analysis result. The canonicalizer then
 * propagates the improved stamp through the method and, for example, removes null-checks when the
 * new stamp states that a value is non-null.
 */
public class StrengthenStampsPhase extends Phase {

    @Override
    protected void run(StructuredGraph graph) {
        for (Node n : graph.getNodes()) {
            if (n instanceof ValueNode && !(n instanceof LimitedValueProxy) && !(n instanceof PhiNode)) {
                /*
                 * The stamp of proxy nodes and phi nodes is inferred automatically, so we do not
                 * need to improve them.
                 */
                ValueNode node = (ValueNode) n;

                /*
                 * First ask the node to improve the stamp itself, to incorporate already improved
                 * input stamps.
                 */
                node.inferStamp();

                Stamp newStamp = strengthen(node.stamp(NodeView.DEFAULT));
                if (newStamp != null) {
                    /*
                     * This assertion no longer holds in corner cases. For example, when a
                     * RawLoadNode is canonicalized to a LoadFieldNode after static analysis, the
                     * stamp cannot be strengthened by StrengthenGraphs. This can happen in
                     * equals/hashCode methods of records which (in the GraalVM 23.1 release branch)
                     * use RawLoadNode during analysis whose field offset is then a known constant
                     * after analysis. Since this phase is already removed in GraalVM 24.0 and
                     * later, there is no need to fix these corner cases.
                     */
                    // assert !parseOnce : "Must be done by StrengthenGraphs";
                    node.setStamp(newStamp);
                }
            }

            if (n instanceof LoadFieldNode) {
                LoadFieldNode node = (LoadFieldNode) n;
                updateStamp(node, toHosted(node.field()).getFieldTypeProfile());

            } else if (n instanceof InstanceOfNode) {
                InstanceOfNode node = (InstanceOfNode) n;
                ObjectStamp newStamp = (ObjectStamp) strengthen(node.getCheckedStamp());
                if (newStamp != null) {
                    assert !parseOnce : "Must be done by StrengthenGraphs";
                    node.replaceAndDelete(graph.addOrUniqueWithInputs(InstanceOfNode.createHelper(newStamp, node.getValue(), node.profile(), node.getAnchor())));
                }

            } else if (n instanceof PiNode) {
                PiNode node = (PiNode) n;
                Stamp newStamp = strengthen(node.piStamp());
                if (newStamp != null) {
                    assert !parseOnce : "Must be done by StrengthenGraphs";
                    node.strengthenPiStamp(newStamp);
                }
            }
        }
    }

    private Stamp strengthen(Stamp s) {
        if (!(s instanceof AbstractObjectStamp)) {
            return null;
        }

        AbstractObjectStamp stamp = (AbstractObjectStamp) s;
        HostedType originalType = toHosted(stamp.type());
        if (originalType == null) {
            return null;
        }

        HostedType strengthenType = originalType.getStrengthenStampType();
        if (originalType.equals(strengthenType)) {
            /* Nothing to strengthen. */
            return null;
        }

        Stamp newStamp;
        if (strengthenType == null) {
            if (stamp.nonNull()) {
                /* We must be in dead code. */
                newStamp = StampFactory.empty(JavaKind.Object);
            } else {
                /* The type its subtypes are not instantiated, the only possible value is null. */
                newStamp = StampFactory.alwaysNull();
            }

        } else {
            if (stamp.isExactType()) {
                /* We must be in dead code. */
                newStamp = StampFactory.empty(JavaKind.Object);
            } else {
                ResolvedJavaType targetType = toTarget(strengthenType);
                if (targetType == null) {
                    return null;
                }
                TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType);
                newStamp = StampFactory.object(typeRef, stamp.nonNull());
            }
        }
        return newStamp;
    }

    private final boolean parseOnce = SubstrateOptions.parseOnce();

    private void updateStamp(ValueNode node, JavaTypeProfile typeProfile) {
        if (node.getStackKind() != JavaKind.Object) {
            return;
        }

        if (typeProfile != null) {
            Stamp newStamp = strengthenStamp(node, typeProfile);
            if (!newStamp.equals(node.stamp(NodeView.DEFAULT))) {
                assert !parseOnce : "Must be done by StrengthenGraphs";
                node.getDebug().log("STAMP UPDATE  method %s  node %s  old %s  new %s%n", node.graph().method().format("%H.%n(%p)"), node, node.stamp(NodeView.DEFAULT), newStamp);
                node.setStamp(newStamp);
            }
        }
    }

    private Stamp strengthenStamp(ValueNode node, JavaTypeProfile typeProfile) {
        ObjectStamp oldStamp = (ObjectStamp) node.stamp(NodeView.DEFAULT);
        HostedType oldType = toHosted(oldStamp.type());

        if (oldStamp.alwaysNull()) {
            /* We cannot make that more precise. */
            return oldStamp;
        }

        boolean nonNull = oldStamp.nonNull() || typeProfile.getNullSeen() == TriState.FALSE;
        ProfiledType[] exactTypes = typeProfile.getTypes();

        if (exactTypes.length == 1) {
            ResolvedJavaType exactType = exactTypes[0].getType();

            assert oldType == null || oldType.isAssignableFrom(exactType);
            if (!oldStamp.isExactType() || !exactType.equals(oldType) || nonNull != oldStamp.nonNull()) {
                ResolvedJavaType targetType = toTarget(exactType);
                if (targetType == null) {
                    return oldStamp;
                }
                TypeReference typeRef = TypeReference.createExactTrusted(targetType);
                return nonNull ? StampFactory.objectNonNull(typeRef) : StampFactory.object(typeRef);
            } else {
                return oldStamp;
            }

        }

        if (exactTypes.length == 0) {
            if (!nonNull) {
                return StampFactory.alwaysNull();
            } else {
                /*
                 * The code after the node is unreachable. We just insert a always-failing guard
                 * after the node and let dead code elimination remove everything after the node.
                 */
                StructuredGraph graph = node.graph();
                FixedWithNextNode insertionPoint;
                if (node instanceof ParameterNode) {
                    /* The whole method is unreachable. */
                    insertionPoint = graph.start();
                } else if (node instanceof InvokeWithExceptionNode) {
                    /* The invoked method never returns normally (but can throw an exception). */
                    insertionPoint = ((InvokeWithExceptionNode) node).next();
                } else {
                    insertionPoint = (FixedWithNextNode) node;
                }
                graph.addAfterFixed(insertionPoint, graph.add(new FixedGuardNode(LogicConstantNode.forBoolean(true, graph), DeoptimizationReason.UnreachedCode, DeoptimizationAction.None, true)));
                return oldStamp;
            }
        }

        ResolvedJavaType baseType;
        if (oldStamp.isExactType()) {
            /* Base type cannot be more precise. */
            baseType = oldType;
        } else {
            assert exactTypes.length > 1;
            assert oldType == null || oldType.isAssignableFrom(exactTypes[0].getType());
            baseType = exactTypes[0].getType();
            for (int i = 1; i < exactTypes.length; i++) {
                assert oldType == null || oldType.isAssignableFrom(exactTypes[i].getType());
                baseType = baseType.findLeastCommonAncestor(exactTypes[i].getType());
            }

            if (oldType != null && !oldType.isAssignableFrom(baseType)) {
                /*
                 * When the original stamp is an interface type, we do not want to weaken that type
                 * with the common base class of all implementation types (which could even be
                 * java.lang.Object).
                 */
                baseType = oldType;
            }
        }

        if (!baseType.equals(oldType) || nonNull != oldStamp.nonNull()) {
            ResolvedJavaType targetType = toTarget(baseType);
            if (targetType == null) {
                return oldStamp;
            }
            TypeReference typeRef = TypeReference.createTrustedWithoutAssumptions(targetType);
            return nonNull ? StampFactory.objectNonNull(typeRef) : StampFactory.object(typeRef);
        }
        return oldStamp;
    }

    protected HostedType toHosted(ResolvedJavaType type) {
        return (HostedType) type;
    }

    protected HostedField toHosted(ResolvedJavaField field) {
        return (HostedField) field;
    }

    protected ResolvedJavaType toTarget(ResolvedJavaType type) {
        return type;
    }
}
