# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This script generates jit/MIROpsGenerated.h (list of MIR instructions)
# from MIROps.yaml, as well as MIR op definitions.

from collections import OrderedDict

import buildconfig
import six
import yaml
from mozbuild.preprocessor import Preprocessor

HEADER_TEMPLATE = """\
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef %(includeguard)s
#define %(includeguard)s

/* This file is generated by jit/GenerateMIRFiles.py. Do not edit! */

%(contents)s

#endif // %(includeguard)s
"""


def generate_header(c_out, includeguard, contents):
    c_out.write(
        HEADER_TEMPLATE
        % {
            "includeguard": includeguard,
            "contents": contents,
        }
    )


def load_yaml(yaml_path):
    # First invoke preprocessor.py so that we can use #ifdef JS_SIMULATOR in
    # the YAML file.
    pp = Preprocessor()
    pp.context.update(buildconfig.defines["ALLDEFINES"])
    pp.out = six.StringIO()
    pp.do_filter("substitution")
    pp.do_include(yaml_path)
    contents = pp.out.getvalue()

    # Load into an OrderedDict to ensure order is preserved. Note: Python 3.7+
    # also preserves ordering for normal dictionaries.
    # Code based on https://stackoverflow.com/a/21912744.
    class OrderedLoader(yaml.Loader):
        pass

    def construct_mapping(loader, node):
        loader.flatten_mapping(node)
        return OrderedDict(loader.construct_pairs(node))

    tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
    OrderedLoader.add_constructor(tag, construct_mapping)
    return yaml.load(contents, OrderedLoader)


type_policies = {
    "Object": "ObjectPolicy",
    "Value": "BoxPolicy",
    "Int32": "UnboxedInt32Policy",
    "BigInt": "BigIntPolicy",
    "Boolean": "BooleanPolicy",
    "Double": "DoublePolicy",
    "String": "StringPolicy",
    "Symbol": "SymbolPolicy",
}


def decide_type_policy(types, no_type_policy):
    if no_type_policy:
        return "public NoTypePolicy::Data"

    if len(types) == 1:
        return "public {}<0>::Data".format(type_policies[types[0]])

    type_num = 0
    mixed_type_policies = []
    for mir_type in types:
        policy = type_policies[mir_type]
        mixed_type_policies.append("{}<{}>".format(policy, type_num))
        type_num += 1

    return "public MixPolicy<{}>::Data".format(", ".join(mixed_type_policies))


mir_base_class = [
    "MNullaryInstruction",
    "MUnaryInstruction",
    "MBinaryInstruction",
    "MTernaryInstruction",
    "MQuaternaryInstruction",
]


gc_pointer_types = [
    "JSObject*",
    "NativeObject*",
    "JSFunction*",
    "BaseScript*",
    "PropertyName*",
    "Shape*",
    "GetterSetter*",
    "JSAtom*",
    "ClassBodyScope*",
    "VarScope*",
    "NamedLambdaObject*",
    "RegExpObject*",
    "JSScript*",
    "LexicalScope*",
]


def gen_mir_class(
    name,
    operands,
    arguments,
    no_type_policy,
    result,
    guard,
    movable,
    folds_to,
    congruent_to,
    alias_set,
    might_alias,
    possibly_calls,
    compute_range,
    can_recover,
    clone,
):
    """Generates class definition for a single MIR opcode."""

    # Generate a MIR opcode class definition.
    # For example:
    # class MGuardIndexIsValidUpdateOrAdd
    #     : public MBinaryInstruction,
    #       public MixPolicy<ObjectPolicy<0>, UnboxedInt32Policy<1>>::Data {
    #  explicit MGuardIndexIsValidUpdateOrAdd(MDefinition* object,
    #                                         MDefinition* index)
    #     : MBinaryInstruction(classOpcode, object, index) {
    #   setGuard();
    #   setMovable();
    #   setResultType(MIRType::Int32);
    #  }
    # public:
    #  INSTRUCTION_HEADER(GetFrameArgument)
    #  TRIVIAL_NEW_WRAPPERS
    #  NAMED_OPERANDS((0, object), (1, index))
    #  AliasSet getAliasSet() const override { return AliasSet::None(); }
    #  bool congruentTo(const MDefinition* ins) const override {
    #    return congruentIfOperandsEqual(ins); }
    #  };
    #

    type_policy = ""
    # MIR op constructor operands.
    mir_operands = []
    # MIR op base class constructor operands.
    mir_base_class_operands = []
    # Types of each constructor operand.
    mir_types = []
    # Items for NAMED_OPERANDS.
    named_operands = []
    if operands:
        current_oper_num = 0
        for oper_name in operands:
            oper = "MDefinition* " + oper_name
            mir_operands.append(oper)
            mir_base_class_operands.append(", " + oper_name)
            # Collect all the MIR argument types to use for determining the
            # ops type policy.
            mir_types.append(operands[oper_name])
            # Collecting named operands for defining accessors.
            named_operands.append("({}, {})".format(current_oper_num, oper_name))
            current_oper_num += 1
        type_policy = decide_type_policy(mir_types, no_type_policy)

    class_name = "M" + name

    assert len(mir_operands) < 5
    base_class = mir_base_class[len(mir_operands)]
    assert base_class
    if base_class != "MNullaryInstruction":
        assert type_policy
        type_policy = ", " + type_policy
    code = "class {} : public {}{} {{\\\n".format(class_name, base_class, type_policy)

    # Arguments to class constructor that require accessors.
    mir_args = []
    if arguments:
        for arg_name in arguments:
            arg_type_sig = arguments[arg_name]
            mir_args.append(arg_type_sig + " " + arg_name)
            if arg_type_sig in gc_pointer_types:
                code += "  CompilerGCPointer<" + arg_type_sig + ">"
            else:
                code += "  " + arg_type_sig
            code += " " + arg_name + "_;\\\n"

    code += "  explicit {}({}) : {}(classOpcode{})".format(
        class_name,
        ", ".join(mir_operands + mir_args),
        base_class,
        "".join(mir_base_class_operands),
    )
    if arguments:
        for arg_name in arguments:
            code += ", " + arg_name + "_(" + arg_name + ")"
    code += " {\\\n"
    if guard:
        code += "    setGuard();\\\n"
    if movable:
        code += "    setMovable();\\\n"
    if result:
        code += "    setResultType(MIRType::{});\\\n".format(result)
    code += "  }\\\n public:\\\n"
    if arguments:
        for arg_name in arguments:
            code += "  " + arguments[arg_name] + " " + arg_name + "() const { "
            code += "return " + arg_name + "_; }\\\n"
    code += "  INSTRUCTION_HEADER({})\\\n".format(name)
    code += "  TRIVIAL_NEW_WRAPPERS\\\n"
    if named_operands:
        code += "  NAMED_OPERANDS({})\\\n".format(", ".join(named_operands))
    if alias_set:
        if alias_set == "custom":
            code += "  AliasSet getAliasSet() const override;\\\n"
        else:
            assert alias_set == "none"
            code += (
                "  AliasSet getAliasSet() const override { "
                "return AliasSet::None(); }\\\n"
            )
    if might_alias:
        code += "  AliasType mightAlias(const MDefinition* store) const override;\\\n"
    if folds_to:
        code += "  MDefinition* foldsTo(TempAllocator& alloc) override;\\\n"
    if congruent_to:
        if congruent_to == "custom":
            code += "  bool congruentTo(const MDefinition* ins) const override;\\\n"
        else:
            assert congruent_to == "if_operands_equal"
            code += (
                "  bool congruentTo(const MDefinition* ins) const override { "
                "return congruentIfOperandsEqual(ins); }\\\n"
            )
    if possibly_calls:
        if possibly_calls == "custom":
            code += "  bool possiblyCalls() const override;\\\n"
        else:
            code += "  bool possiblyCalls() const override { return true; }\\\n"
    if compute_range:
        code += "  void computeRange(TempAllocator& alloc) override;\\\n"
    if can_recover:
        code += "  [[nodiscard]] bool writeRecoverData(\\\n"
        code += "    CompactBufferWriter& writer) const override;\\\n"
        if can_recover == "custom":
            code += "  bool canRecoverOnBailout() const override;\\\n"
        else:
            code += "  bool canRecoverOnBailout() const override { return true; }\\\n"
    if clone:
        code += "  ALLOW_CLONE(" + class_name + ")\\\n"
    code += "};\\\n"
    return code


def gen_non_gc_pointer_type_assertions(seen_types):
    """Generates a list of static assertions used to ensure that all argument
    types seen are not derived from gc::Cell, ensuring that gc pointer arguments
    are added to the gc_pointer_types list.
    """
    assertions = []

    for seen_type in sorted(seen_types):
        assertions.append(
            "static_assert(!std::is_base_of_v<gc::Cell, " + seen_type.strip("*") + ">, "
            '"Ensure that '
            + seen_type.strip("*")
            + ' is added to the gc_pointer_types list in GenerateMIRFiles.py."'
            ");"
        )

    return assertions


def generate_mir_header(c_out, yaml_path):
    """Generate MIROpsGenerated.h from MIROps.yaml. The generated file
    has a list of MIR ops and boilerplate for MIR op definitions.
    """

    data = load_yaml(yaml_path)

    # MIR_OPCODE_LIST items. Stores the name of each MIR op.
    ops_items = []

    # Generated MIR op class definitions.
    mir_op_classes = []

    # Unique and non gc pointer types seen for arguments to the MIR constructor.
    seen_non_gc_pointer_argument_types = set()

    for op in data:
        name = op["name"]

        ops_items.append("_({})".format(name))

        gen_boilerplate = op.get("gen_boilerplate", True)
        assert isinstance(gen_boilerplate, bool)

        if gen_boilerplate:
            operands = op.get("operands", None)
            assert operands is None or isinstance(operands, OrderedDict)

            arguments = op.get("arguments", None)
            assert arguments is None or isinstance(arguments, OrderedDict)

            no_type_policy = op.get("type_policy", None)
            assert no_type_policy is None or no_type_policy == "none"

            result = op.get("result_type", None)
            assert result is None or isinstance(result, str)

            guard = op.get("guard", None)
            assert guard is None or True

            movable = op.get("movable", None)
            assert movable is None or True

            folds_to = op.get("folds_to", None)
            assert folds_to is None or folds_to == "custom"

            congruent_to = op.get("congruent_to", None)
            assert (
                congruent_to is None
                or congruent_to == "if_operands_equal"
                or congruent_to == "custom"
            )

            alias_set = op.get("alias_set", None)
            assert alias_set is None or True or isinstance(alias_set, str)

            might_alias = op.get("might_alias", None)
            assert might_alias is None or might_alias == "custom"

            possibly_calls = op.get("possibly_calls", None)
            assert possibly_calls is None or True or possibly_calls == "custom"

            compute_range = op.get("compute_range", None)
            assert compute_range is None or compute_range == "custom"

            can_recover = op.get("can_recover", None)
            assert can_recover is None or True or can_recover == "custom"

            clone = op.get("clone", None)
            assert clone is None or True

            code = gen_mir_class(
                name,
                operands,
                arguments,
                no_type_policy,
                result,
                guard,
                movable,
                folds_to,
                congruent_to,
                alias_set,
                might_alias,
                possibly_calls,
                compute_range,
                can_recover,
                clone,
            )
            mir_op_classes.append(code)

            if arguments:
                for argument in arguments:
                    arg_type = arguments[argument]
                    if arg_type not in gc_pointer_types:
                        seen_non_gc_pointer_argument_types.add(arg_type)

    contents = "#define MIR_OPCODE_LIST(_)\\\n"
    contents += "\\\n".join(ops_items)
    contents += "\n\n"

    contents += "#define MIR_OPCODE_CLASS_GENERATED \\\n"
    contents += "\\\n".join(mir_op_classes)
    contents += "\n\n"

    contents += "#define NON_GC_POINTER_TYPE_ASSERTIONS_GENERATED \\\n"
    contents += "\\\n".join(
        gen_non_gc_pointer_type_assertions(seen_non_gc_pointer_argument_types)
    )
    contents += "\n\n"

    generate_header(c_out, "jit_MIROpsGenerated_h", contents)
