# vim: set ts=8 sts=4 et sw=4 tw=99:
# 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 checks that SpiderMonkey MacroAssembler methods are properly
# annotated.
#
# The MacroAssembler has one interface for all platforms, but it might have one
# definition per platform. The code of the MacroAssembler use a macro to
# annotate the method declarations, in order to delete the function if it is not
# present on the current platform, and also to locate the files in which the
# methods are defined.
#
# This script scans the MacroAssembler.h header, for method declarations.
# It also scans MacroAssembler-/arch/.cpp, MacroAssembler-/arch/-inl.h, and
# MacroAssembler-inl.h for method definitions. The result of both scans are
# uniformized, and compared, to determine if the MacroAssembler.h header as
# proper methods annotations.
# ----------------------------------------------------------------------------

from __future__ import absolute_import
from __future__ import print_function

import difflib
import os
import re
import sys

architecture_independent = set(['generic'])
all_unsupported_architectures_names = set(['mips32', 'mips64', 'mips_shared'])
all_architecture_names = set(['x86', 'x64', 'arm', 'arm64'])
all_shared_architecture_names = set(['x86_shared', 'arm', 'arm64'])

reBeforeArg = "(?<=[(,\s])"
reArgType = "(?P<type>[\w\s:*&]+)"
reArgName = "(?P<name>\s\w+)"
reArgDefault = "(?P<default>(?:\s=[^,)]+)?)"
reAfterArg = "(?=[,)])"
reMatchArg = re.compile(reBeforeArg + reArgType +
                        reArgName + reArgDefault + reAfterArg)


def get_normalized_signatures(signature, fileAnnot=None):
    # Remove static
    signature = signature.replace('static', '')
    # Remove semicolon.
    signature = signature.replace(';', ' ')
    # Normalize spaces.
    signature = re.sub(r'\s+', ' ', signature).strip()
    # Match arguments, and keep only the type.
    signature = reMatchArg.sub('\g<type>', signature)
    # Remove class name
    signature = signature.replace('MacroAssembler::', '')

    # Extract list of architectures
    archs = ['generic']
    if fileAnnot:
        archs = [fileAnnot['arch']]

    if 'DEFINED_ON(' in signature:
        archs = re.sub(
            r'.*DEFINED_ON\((?P<archs>[^()]*)\).*', '\g<archs>', signature).split(',')
        archs = [a.strip() for a in archs]
        signature = re.sub(r'\s+DEFINED_ON\([^()]*\)', '', signature)

    elif 'PER_ARCH' in signature:
        archs = all_architecture_names
        signature = re.sub(r'\s+PER_ARCH', '', signature)

    elif 'PER_SHARED_ARCH' in signature:
        archs = all_shared_architecture_names
        signature = re.sub(r'\s+PER_SHARED_ARCH', '', signature)

    elif 'OOL_IN_HEADER' in signature:
        assert archs == ['generic']
        signature = re.sub(r'\s+OOL_IN_HEADER', '', signature)

    else:
        # No signature annotation, the list of architectures remains unchanged.
        pass

    # Extract inline annotation
    inline = False
    if fileAnnot:
        inline = fileAnnot['inline']

    if 'inline ' in signature:
        signature = re.sub(r'inline\s+', '', signature)
        inline = True

    inlinePrefx = ''
    if inline:
        inlinePrefx = 'inline '
    signatures = [
        {'arch': a, 'sig': inlinePrefx + signature}
        for a in archs
    ]

    return signatures


file_suffixes = set([
    a.replace('_', '-') for a in
    all_architecture_names.union(all_shared_architecture_names)
                          .union(all_unsupported_architectures_names)
])


def get_file_annotation(filename):
    origFilename = filename
    filename = filename.split('/')[-1]

    inline = False
    if filename.endswith('.cpp'):
        filename = filename[:-len('.cpp')]
    elif filename.endswith('-inl.h'):
        inline = True
        filename = filename[:-len('-inl.h')]
    elif filename.endswith('.h'):
        # This allows the definitions block in MacroAssembler.h to be
        # style-checked.
        inline = True
        filename = filename[:-len('.h')]
    else:
        raise Exception('unknown file name', origFilename)

    arch = 'generic'
    for suffix in file_suffixes:
        if filename == 'MacroAssembler-' + suffix:
            arch = suffix
            break

    return {
        'inline': inline,
        'arch': arch.replace('-', '_')
    }


def get_macroassembler_definitions(filename):
    try:
        fileAnnot = get_file_annotation(filename)
    except Exception:
        return []

    style_section = False
    code_section = False
    lines = ''
    signatures = []
    with open(filename) as f:
        for line in f:
            if '//{{{ check_macroassembler_style' in line:
                style_section = True
            elif '//}}} check_macroassembler_style' in line:
                style_section = False
            if not style_section:
                continue

            line = re.sub(r'//.*', '', line)
            if line.startswith('{') or line.strip() == "{}":
                if 'MacroAssembler::' in lines:
                    signatures.extend(
                        get_normalized_signatures(lines, fileAnnot))
                if line.strip() != "{}":  # Empty declaration, no need to declare
                    # a new code section
                    code_section = True
                continue
            if line.startswith('}'):
                code_section = False
                lines = ''
                continue
            if code_section:
                continue

            if len(line.strip()) == 0:
                lines = ''
                continue
            lines = lines + line
            # Continue until we have a complete declaration
            if '{' not in lines:
                continue
            # Skip variable declarations
            if ')' not in lines:
                lines = ''
                continue

    return signatures


def get_macroassembler_declaration(filename):
    style_section = False
    lines = ''
    signatures = []
    with open(filename) as f:
        for line in f:
            if '//{{{ check_macroassembler_decl_style' in line:
                style_section = True
            elif '//}}} check_macroassembler_decl_style' in line:
                style_section = False
            if not style_section:
                continue

            line = re.sub(r'//.*', '', line)
            if len(line.strip()) == 0:
                lines = ''
                continue
            lines = lines + line
            # Continue until we have a complete declaration
            if ';' not in lines:
                continue
            # Skip variable declarations
            if ')' not in lines:
                lines = ''
                continue

            signatures.extend(get_normalized_signatures(lines))
            lines = ''

    return signatures


def append_signatures(d, sigs):
    for s in sigs:
        if s['sig'] not in d:
            d[s['sig']] = []
        d[s['sig']].append(s['arch'])
    return d


def generate_file_content(signatures):
    output = []
    for s in sorted(signatures.keys()):
        archs = set(sorted(signatures[s]))
        archs -= all_unsupported_architectures_names
        if len(archs.symmetric_difference(architecture_independent)) == 0:
            output.append(s + ';\n')
            if s.startswith('inline'):
                # TODO, bug 1432600: This is mistaken for OOL_IN_HEADER
                # functions.  (Such annotation is already removed by the time
                # this function sees the signature here.)
                output.append('    is defined in MacroAssembler-inl.h\n')
            else:
                output.append('    is defined in MacroAssembler.cpp\n')
        else:
            if len(archs.symmetric_difference(all_architecture_names)) == 0:
                output.append(s + ' PER_ARCH;\n')
            elif len(archs.symmetric_difference(all_shared_architecture_names)) == 0:
                output.append(s + ' PER_SHARED_ARCH;\n')
            else:
                output.append(
                    s + ' DEFINED_ON(' + ', '.join(sorted(archs)) + ');\n')
            for a in sorted(archs):
                a = a.replace('_', '-')
                masm = '%s/MacroAssembler-%s' % (a, a)
                if s.startswith('inline'):
                    output.append('    is defined in %s-inl.h\n' % masm)
                else:
                    output.append('    is defined in %s.cpp\n' % masm)
    return output


def check_style():
    # We read from the header file the signature of each function.
    decls = dict()      # type: dict(signature => ['x86', 'x64'])

    # We infer from each file the signature of each MacroAssembler function.
    defs = dict()       # type: dict(signature => ['x86', 'x64'])

    root_dir = os.path.join('js', 'src', 'jit')
    for dirpath, dirnames, filenames in os.walk(root_dir):
        for filename in filenames:
            if 'MacroAssembler' not in filename:
                continue

            filepath = os.path.join(dirpath, filename).replace('\\', '/')

            if filepath.endswith('MacroAssembler.h'):
                decls = append_signatures(
                    decls, get_macroassembler_declaration(filepath))
            defs = append_signatures(
                defs, get_macroassembler_definitions(filepath))

    if not decls or not defs:
        raise Exception("Did not find any definitions or declarations")

    # Compare declarations and definitions output.
    difflines = difflib.unified_diff(generate_file_content(decls),
                                     generate_file_content(defs),
                                     fromfile='check_macroassembler_style.py declared syntax',
                                     tofile='check_macroassembler_style.py found definitions')
    ok = True
    for diffline in difflines:
        ok = False
        print(diffline, end='')

    return ok


def main():
    ok = check_style()

    if ok:
        print('TEST-PASS | check_macroassembler_style.py | ok')
    else:
        print('TEST-UNEXPECTED-FAIL | check_macroassembler_style.py | actual output does not match expected output;  diff is above')  # noqa: E501

    sys.exit(0 if ok else 1)


if __name__ == '__main__':
    main()
