llvm/openmp/runtime/tools/message-converter.py

#!/usr/bin/env python3

#
# //===----------------------------------------------------------------------===//
# //
# // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# // See https://llvm.org/LICENSE.txt for license information.
# // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
# //
# //===----------------------------------------------------------------------===//
#

import argparse
import datetime
import os
import platform
import re
import sys
from libomputils import ScriptError, error


class ParseMessageDataError(ScriptError):
    """Convenience class for parsing message data file errors"""

    def __init__(self, filename, line, msg):
        super(ParseMessageDataError, self).__init__(msg)
        self.filename = filename
        self.line = line


def parse_error(filename, line, msg):
    raise ParseMessageDataError(filename, line, msg)


def display_language_id(inputFile):
    """Quickly parse file for LangId and print it"""
    regex = re.compile(r'^LangId\s+"([0-9]+)"')
    with open(inputFile, encoding="utf-8") as f:
        for line in f:
            m = regex.search(line)
            if not m:
                continue
            print(m.group(1))


class Message(object):
    special = {
        "n": "\n",
        "t": "\t",
    }

    def __init__(self, lineNumber, name, text):
        self.lineNumber = lineNumber
        self.name = name
        self.text = text

    def toSrc(self):
        if platform.system() == "Windows":
            return re.sub(r"%([0-9])\$(s|l?[du])", r"%\1!\2!", self.text)
        return str(self.text)

    def toMC(self):
        retval = self.toSrc()
        for special, substitute in Message.special.items():
            retval = re.sub(r"\\{}".format(special), substitute, retval)
        return retval


class MessageData(object):
    """
    Convenience class representing message data parsed from i18n/* files

    Generate these objects using static create() factory method
    """

    sectionInfo = {
        "meta": {"short": "prp", "long": "meta", "set": 1, "base": 1 << 16},
        "strings": {"short": "str", "long": "strings", "set": 2, "base": 2 << 16},
        "formats": {"short": "fmt", "long": "formats", "set": 3, "base": 3 << 16},
        "messages": {"short": "msg", "long": "messages", "set": 4, "base": 4 << 16},
        "hints": {"short": "hnt", "long": "hints", "set": 5, "base": 5 << 16},
    }
    orderedSections = ["meta", "strings", "formats", "messages", "hints"]

    def __init__(self):
        self.filename = None
        self.sections = {}

    def getMeta(self, name):
        metaList = self.sections["meta"]
        for meta in metaList:
            if meta.name == name:
                return meta.text
        error(
            'No "{}" detected in meta data' " for file {}".format(name, self.filename)
        )

    @staticmethod
    def create(inputFile):
        """Creates MessageData object from inputFile"""
        data = MessageData()
        data.filename = os.path.abspath(inputFile)
        obsolete = 1
        sectionRegex = re.compile(r"-\*- ([a-zA-Z0-9_]+) -\*-")
        keyValueRegex = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)\s+"(.*)"')
        moreValueRegex = re.compile(r'"(.*)"')

        with open(inputFile, "r", encoding="utf-8") as f:
            currentSection = None
            currentKey = None
            for lineNumber, line in enumerate(f, 1):
                line = line.strip()
                # Skip empty lines
                if not line:
                    continue
                # Skip comment lines
                if line.startswith("#"):
                    continue
                # Matched a section header
                match = sectionRegex.search(line)
                if match:
                    currentSection = match.group(1).lower()
                    if currentSection in data.sections:
                        parse_error(
                            inputFile,
                            lineNumber,
                            "section: {} already defined".format(currentSection),
                        )
                    data.sections[currentSection] = []
                    continue
                # Matched a Key "Value" line (most lines)
                match = keyValueRegex.search(line)
                if match:
                    if not currentSection:
                        parse_error(inputFile, lineNumber, "no section defined yet.")
                    key = match.group(1)
                    if key == "OBSOLETE":
                        key = "OBSOLETE{}".format(obsolete)
                        obsolete += 1
                    value = match.group(2)
                    currentKey = key
                    data.sections[currentSection].append(
                        Message(lineNumber, key, value)
                    )
                    continue
                # Matched a Continuation of string line
                match = moreValueRegex.search(line)
                if match:
                    value = match.group(1)
                    if not currentSection:
                        parse_error(inputFile, lineNumber, "no section defined yet.")
                    if not currentKey:
                        parse_error(inputFile, lineNumber, "no key defined yet.")
                    data.sections[currentSection][-1].text += value
                    continue
                # Unknown line syntax
                parse_error(inputFile, lineNumber, "bad line:\n{}".format(line))
        return data


def insert_header(f, data, commentChar="//"):
    f.write(
        "{0} Do not edit this file! {0}\n"
        "{0} The file was generated from"
        " {1} by {2} on {3}. {0}\n\n".format(
            commentChar,
            os.path.basename(data.filename),
            os.path.basename(__file__),
            datetime.datetime.now().ctime(),
        )
    )


def generate_enum_file(enumFile, prefix, data):
    """Create the include file with message enums"""
    global g_sections
    with open(enumFile, "w") as f:
        insert_header(f, data)
        f.write(
            "enum {0}_id {1}\n"
            "\n"
            "    // A special id for absence of message.\n"
            "    {0}_null = 0,\n"
            "\n".format(prefix, "{")
        )
        for section in MessageData.orderedSections:
            messages = data.sections[section]
            info = MessageData.sectionInfo[section]
            shortName = info["short"]
            longName = info["long"]
            base = info["base"]
            setIdx = info["set"]
            f.write(
                "    // Set #{}, {}.\n"
                "    {}_{}_first = {},\n".format(
                    setIdx, longName, prefix, shortName, base
                )
            )
            for message in messages:
                f.write("    {}_{}_{},\n".format(prefix, shortName, message.name))
            f.write("    {}_{}_last,\n\n".format(prefix, shortName))
        f.write(
            "    {0}_xxx_lastest\n\n"
            "{1}; // enum {0}_id\n\n"
            "typedef enum {0}_id  {0}_id_t;\n\n\n"
            "// end of file //\n".format(prefix, "}")
        )


def generate_signature_file(signatureFile, data):
    """Create the signature file"""
    sigRegex = re.compile(r"(%[0-9]\$(s|l?[du]))")
    with open(signatureFile, "w") as f:
        f.write("// message catalog signature file //\n\n")
        for section in MessageData.orderedSections:
            messages = data.sections[section]
            longName = MessageData.sectionInfo[section]["long"]
            f.write("-*- {}-*-\n\n".format(longName.upper()))
            for message in messages:
                sigs = sorted(list(set([a for a, b in sigRegex.findall(message.text)])))
                i = 0
                # Insert empty placeholders if necessary
                while i != len(sigs):
                    num = i + 1
                    if not sigs[i].startswith("%{}".format(num)):
                        sigs.insert(i, "%{}$-".format(num))
                    else:
                        i += 1
                f.write("{:<40} {}\n".format(message.name, " ".join(sigs)))
            f.write("\n")
        f.write("// end of file //\n")


def generate_default_messages_file(defaultFile, prefix, data):
    """Create the include file with message strings organized"""
    with open(defaultFile, "w", encoding="utf-8") as f:
        insert_header(f, data)
        for section in MessageData.orderedSections:
            f.write(
                "static char const *\n"
                "__{}_default_{}[] =\n"
                "    {}\n"
                "        NULL,\n".format(prefix, section, "{")
            )
            messages = data.sections[section]
            for message in messages:
                f.write('        "{}",\n'.format(message.toSrc()))
            f.write("        NULL\n" "    {};\n\n".format("}"))
        f.write(
            "struct kmp_i18n_section {0}\n"
            "    int           size;\n"
            "    char const ** str;\n"
            "{1}; // struct kmp_i18n_section\n"
            "typedef struct kmp_i18n_section  kmp_i18n_section_t;\n\n"
            "static kmp_i18n_section_t\n"
            "__{2}_sections[] =\n"
            "    {0}\n"
            "        {0} 0, NULL {1},\n".format("{", "}", prefix)
        )

        for section in MessageData.orderedSections:
            messages = data.sections[section]
            f.write(
                "        {} {}, __{}_default_{} {},\n".format(
                    "{", len(messages), prefix, section, "}"
                )
            )
        numSections = len(MessageData.orderedSections)
        f.write(
            "        {0} 0, NULL {1}\n"
            "    {1};\n\n"
            "struct kmp_i18n_table {0}\n"
            "    int                   size;\n"
            "    kmp_i18n_section_t *  sect;\n"
            "{1}; // struct kmp_i18n_table\n"
            "typedef struct kmp_i18n_table  kmp_i18n_table_t;\n\n"
            "static kmp_i18n_table_t __kmp_i18n_default_table =\n"
            "    {0}\n"
            "        {3},\n"
            "        __{2}_sections\n"
            "    {1};\n\n"
            "// end of file //\n".format("{", "}", prefix, numSections)
        )


def generate_message_file_unix(messageFile, data):
    """
    Create the message file for Unix OSes

    Encoding is in UTF-8
    """
    with open(messageFile, "w", encoding="utf-8") as f:
        insert_header(f, data, commentChar="$")
        f.write('$quote "\n\n')
        for section in MessageData.orderedSections:
            setIdx = MessageData.sectionInfo[section]["set"]
            f.write(
                "$ ------------------------------------------------------------------------------\n"
                "$ {}\n"
                "$ ------------------------------------------------------------------------------\n\n"
                "$set {}\n\n".format(section, setIdx)
            )
            messages = data.sections[section]
            for num, message in enumerate(messages, 1):
                f.write('{} "{}"\n'.format(num, message.toSrc()))
            f.write("\n")
        f.write("\n$ end of file $")


def generate_message_file_windows(messageFile, data):
    """
    Create the message file for Windows OS

    Encoding is in UTF-16LE
    """
    language = data.getMeta("Language")
    langId = data.getMeta("LangId")
    with open(messageFile, "w", encoding="utf-16-le") as f:
        insert_header(f, data, commentChar=";")
        f.write("\nLanguageNames = ({0}={1}:msg_{1})\n\n".format(language, langId))
        f.write("FacilityNames=(\n")
        for section in MessageData.orderedSections:
            setIdx = MessageData.sectionInfo[section]["set"]
            shortName = MessageData.sectionInfo[section]["short"]
            f.write(" {}={}\n".format(shortName, setIdx))
        f.write(")\n\n")

        for section in MessageData.orderedSections:
            shortName = MessageData.sectionInfo[section]["short"]
            n = 0
            messages = data.sections[section]
            for message in messages:
                n += 1
                f.write(
                    "MessageId={}\n"
                    "Facility={}\n"
                    "Language={}\n"
                    "{}\n.\n\n".format(n, shortName, language, message.toMC())
                )
        f.write("\n; end of file ;")


def main():
    parser = argparse.ArgumentParser(description="Generate message data files")
    parser.add_argument(
        "--lang-id",
        action="store_true",
        help="Print language identifier of the message catalog source file",
    )
    parser.add_argument(
        "--prefix",
        default="kmp_i18n",
        help="Prefix to be used for all C identifiers (type and variable names)"
        " in enum and default message files.",
    )
    parser.add_argument("--enum", metavar="FILE", help="Generate enum file named FILE")
    parser.add_argument(
        "--default", metavar="FILE", help="Generate default messages file named FILE"
    )
    parser.add_argument(
        "--signature", metavar="FILE", help="Generate signature file named FILE"
    )
    parser.add_argument(
        "--message", metavar="FILE", help="Generate message file named FILE"
    )
    parser.add_argument("inputfile")
    commandArgs = parser.parse_args()

    if commandArgs.lang_id:
        display_language_id(commandArgs.inputfile)
        return
    data = MessageData.create(commandArgs.inputfile)
    prefix = commandArgs.prefix
    if commandArgs.enum:
        generate_enum_file(commandArgs.enum, prefix, data)
    if commandArgs.default:
        generate_default_messages_file(commandArgs.default, prefix, data)
    if commandArgs.signature:
        generate_signature_file(commandArgs.signature, data)
    if commandArgs.message:
        if platform.system() == "Windows":
            generate_message_file_windows(commandArgs.message, data)
        else:
            generate_message_file_unix(commandArgs.message, data)


if __name__ == "__main__":
    try:
        main()
    except ScriptError as e:
        print("error: {}".format(e))
        sys.exit(1)

# end of file