#!/usr/bin/env python3
#
# Preprocessor that makes asserts easier to debug.
#
# Example:
# ./scripts/prettyasserts.py -p LFS_ASSERT lfs.c -o lfs.a.c
#
# Copyright (c) 2022, The littlefs authors.
# Copyright (c) 2020, Arm Limited. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#

import re
import sys

# NOTE the use of macros here helps keep a consistent stack depth which
# tools may rely on.
#
# If compilation errors are noisy consider using -ftrack-macro-expansion=0.
#

LIMIT = 16

CMP = {
    '==': 'eq',
    '!=': 'ne',
    '<=': 'le',
    '>=': 'ge',
    '<':  'lt',
    '>':  'gt',
}

LEXEMES = {
    'ws':       [r'(?:\s|\n|#.*?\n|//.*?\n|/\*.*?\*/)+'],
    'assert':   ['assert'],
    'arrow':    ['=>'],
    'string':   [r'"(?:\\.|[^"])*"', r"'(?:\\.|[^'])\'"],
    'paren':    ['\(', '\)'],
    'cmp':      CMP.keys(),
    'logic':    ['\&\&', '\|\|'],
    'sep':      [':', ';', '\{', '\}', ','],
    'op':       ['->'], # specifically ops that conflict with cmp
}


def openio(path, mode='r', buffering=-1):
    # allow '-' for stdin/stdout
    if path == '-':
        if mode == 'r':
            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
        else:
            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
    else:
        return open(path, mode, buffering)

def write_header(f, limit=LIMIT):
    f.writeln("// Generated by %s:" % sys.argv[0])
    f.writeln("//")
    f.writeln("// %s" % ' '.join(sys.argv))
    f.writeln("//")
    f.writeln()

    f.writeln("#include <stdbool.h>")
    f.writeln("#include <stdint.h>")
    f.writeln("#include <inttypes.h>")
    f.writeln("#include <stdio.h>")
    f.writeln("#include <string.h>")
    f.writeln("#include <signal.h>")
    # give source a chance to define feature macros
    f.writeln("#undef _FEATURES_H")
    f.writeln()

    # write print macros
    f.writeln("__attribute__((unused))")
    f.writeln("static void __pretty_assert_print_bool(")
    f.writeln("        const void *v, size_t size) {")
    f.writeln("    (void)size;")
    f.writeln("    printf(\"%s\", *(const bool*)v ? \"true\" : \"false\");")
    f.writeln("}")
    f.writeln()
    f.writeln("__attribute__((unused))")
    f.writeln("static void __pretty_assert_print_int(")
    f.writeln("        const void *v, size_t size) {")
    f.writeln("    (void)size;")
    f.writeln("    printf(\"%\"PRIiMAX, *(const intmax_t*)v);")
    f.writeln("}")
    f.writeln()
    f.writeln("__attribute__((unused))")
    f.writeln("static void __pretty_assert_print_mem(")
    f.writeln("        const void *v, size_t size) {")
    f.writeln("    const uint8_t *v_ = v;")
    f.writeln("    printf(\"\\\"\");")
    f.writeln("    for (size_t i = 0; i < size && i < %d; i++) {" % limit)
    f.writeln("        if (v_[i] >= ' ' && v_[i] <= '~') {")
    f.writeln("            printf(\"%c\", v_[i]);")
    f.writeln("        } else {")
    f.writeln("            printf(\"\\\\x%02x\", v_[i]);")
    f.writeln("        }")
    f.writeln("    }")
    f.writeln("    if (size > %d) {" % limit)
    f.writeln("        printf(\"...\");")
    f.writeln("    }")
    f.writeln("    printf(\"\\\"\");")
    f.writeln("}")
    f.writeln()
    f.writeln("__attribute__((unused))")
    f.writeln("static void __pretty_assert_print_str(")
    f.writeln("        const void *v, size_t size) {")
    f.writeln("    __pretty_assert_print_mem(v, size);")
    f.writeln("}")
    f.writeln()
    f.writeln("__attribute__((unused, noinline))")
    f.writeln("static void __pretty_assert_fail(")
    f.writeln("        const char *file, int line,")
    f.writeln("        void (*type_print_cb)(const void*, size_t),")
    f.writeln("        const char *cmp,")
    f.writeln("        const void *lh, size_t lsize,")
    f.writeln("        const void *rh, size_t rsize) {")
    f.writeln("    printf(\"%s:%d:assert: assert failed with \", file, line);")
    f.writeln("    type_print_cb(lh, lsize);")
    f.writeln("    printf(\", expected %s \", cmp);")
    f.writeln("    type_print_cb(rh, rsize);")
    f.writeln("    printf(\"\\n\");")
    f.writeln("    fflush(NULL);")
    f.writeln("    raise(SIGABRT);")
    f.writeln("}")
    f.writeln()

    # write assert macros
    for op, cmp in sorted(CMP.items()):
        f.writeln("#define __PRETTY_ASSERT_BOOL_%s(lh, rh) do { \\"
            % cmp.upper())
        f.writeln("    bool _lh = !!(lh); \\")
        f.writeln("    bool _rh = !!(rh); \\")
        f.writeln("    if (!(_lh %s _rh)) { \\" % op)
        f.writeln("        __pretty_assert_fail( \\")
        f.writeln("                __FILE__, __LINE__, \\")
        f.writeln("                __pretty_assert_print_bool, \"%s\", \\"
            % cmp)
        f.writeln("                &_lh, 0, \\")
        f.writeln("                &_rh, 0); \\")
        f.writeln("    } \\")
        f.writeln("} while (0)")
    for op, cmp in sorted(CMP.items()):
        f.writeln("#define __PRETTY_ASSERT_INT_%s(lh, rh) do { \\"
            % cmp.upper())
        f.writeln("    __typeof__(lh) _lh = lh; \\")
        f.writeln("    __typeof__(lh) _rh = rh; \\")
        f.writeln("    if (!(_lh %s _rh)) { \\" % op)
        f.writeln("        __pretty_assert_fail( \\")
        f.writeln("                __FILE__, __LINE__, \\")
        f.writeln("                __pretty_assert_print_int, \"%s\", \\"
            % cmp)
        f.writeln("                &(intmax_t){_lh}, 0, \\")
        f.writeln("                &(intmax_t){_rh}, 0); \\")
        f.writeln("    } \\")
        f.writeln("} while (0)")
    for op, cmp in sorted(CMP.items()):
        f.writeln("#define __PRETTY_ASSERT_MEM_%s(lh, rh, size) do { \\"
            % cmp.upper())
        f.writeln("    const void *_lh = lh; \\")
        f.writeln("    const void *_rh = rh; \\")
        f.writeln("    if (!(memcmp(_lh, _rh, size) %s 0)) { \\" % op)
        f.writeln("        __pretty_assert_fail( \\")
        f.writeln("                __FILE__, __LINE__, \\")
        f.writeln("                __pretty_assert_print_mem, \"%s\", \\"
            % cmp)
        f.writeln("                _lh, size, \\")
        f.writeln("                _rh, size); \\")
        f.writeln("    } \\")
        f.writeln("} while (0)")
    for op, cmp in sorted(CMP.items()):
        f.writeln("#define __PRETTY_ASSERT_STR_%s(lh, rh) do { \\"
            % cmp.upper())
        f.writeln("    const char *_lh = lh; \\")
        f.writeln("    const char *_rh = rh; \\")
        f.writeln("    if (!(strcmp(_lh, _rh) %s 0)) { \\" % op)
        f.writeln("        __pretty_assert_fail( \\")
        f.writeln("                __FILE__, __LINE__, \\")
        f.writeln("                __pretty_assert_print_str, \"%s\", \\"
            % cmp)
        f.writeln("                _lh, strlen(_lh), \\")
        f.writeln("                _rh, strlen(_rh)); \\")
        f.writeln("    } \\")
        f.writeln("} while (0)")
    f.writeln()
    f.writeln()

def mkassert(type, cmp, lh, rh, size=None):
    if size is not None:
        return ("__PRETTY_ASSERT_%s_%s(%s, %s, %s)"
            % (type.upper(), cmp.upper(), lh, rh, size))
    else:
        return ("__PRETTY_ASSERT_%s_%s(%s, %s)"
            % (type.upper(), cmp.upper(), lh, rh))


# simple recursive descent parser
class ParseFailure(Exception):
    def __init__(self, expected, found):
        self.expected = expected
        self.found = found

    def __str__(self):
        return "expected %r, found %s..." % (
            self.expected, repr(self.found)[:70])

class Parser:
    def __init__(self, in_f, lexemes=LEXEMES):
        p = '|'.join('(?P<%s>%s)' % (n, '|'.join(l))
            for n, l in lexemes.items())
        p = re.compile(p, re.DOTALL)
        data = in_f.read()
        tokens = []
        line = 1
        col = 0
        while True:
            m = p.search(data)
            if m:
                if m.start() > 0:
                    tokens.append((None, data[:m.start()], line, col))
                tokens.append((m.lastgroup, m.group(), line, col))
                data = data[m.end():]
            else:
                tokens.append((None, data, line, col))
                break
        self.tokens = tokens
        self.off = 0

    def lookahead(self, *pattern):
        if self.off < len(self.tokens):
            token = self.tokens[self.off]
            if token[0] in pattern or token[1] in pattern:
                self.m = token[1]
                return self.m
        self.m = None
        return self.m

    def accept(self, *patterns):
        m = self.lookahead(*patterns)
        if m is not None:
            self.off += 1
        return m

    def expect(self, *patterns):
        m = self.accept(*patterns)
        if not m:
            raise ParseFailure(patterns, self.tokens[self.off:])
        return m

    def push(self):
        return self.off

    def pop(self, state):
        self.off = state

def p_assert(p):
    state = p.push()

    # assert(memcmp(a,b,size) cmp 0)?
    try:
        p.expect('assert') ; p.accept('ws')
        p.expect('(') ; p.accept('ws')
        p.expect('memcmp') ; p.accept('ws')
        p.expect('(') ; p.accept('ws')
        lh = p_expr(p) ; p.accept('ws')
        p.expect(',') ; p.accept('ws')
        rh = p_expr(p) ; p.accept('ws')
        p.expect(',') ; p.accept('ws')
        size = p_expr(p) ; p.accept('ws')
        p.expect(')') ; p.accept('ws')
        cmp = p.expect('cmp') ; p.accept('ws')
        p.expect('0') ; p.accept('ws')
        p.expect(')')
        return mkassert('mem', CMP[cmp], lh, rh, size)
    except ParseFailure:
        p.pop(state)

    # assert(strcmp(a,b) cmp 0)?
    try:
        p.expect('assert') ; p.accept('ws')
        p.expect('(') ; p.accept('ws')
        p.expect('strcmp') ; p.accept('ws')
        p.expect('(') ; p.accept('ws')
        lh = p_expr(p) ; p.accept('ws')
        p.expect(',') ; p.accept('ws')
        rh = p_expr(p) ; p.accept('ws')
        p.expect(')') ; p.accept('ws')
        cmp = p.expect('cmp') ; p.accept('ws')
        p.expect('0') ; p.accept('ws')
        p.expect(')')
        return mkassert('str', CMP[cmp], lh, rh)
    except ParseFailure:
        p.pop(state)

    # assert(a cmp b)?
    try:
        p.expect('assert') ; p.accept('ws')
        p.expect('(') ; p.accept('ws')
        lh = p_expr(p) ; p.accept('ws')
        cmp = p.expect('cmp') ; p.accept('ws')
        rh = p_expr(p) ; p.accept('ws')
        p.expect(')')
        return mkassert('int', CMP[cmp], lh, rh)
    except ParseFailure:
        p.pop(state)

    # assert(a)?
    p.expect('assert') ; p.accept('ws')
    p.expect('(') ; p.accept('ws')
    lh = p_exprs(p) ; p.accept('ws')
    p.expect(')')
    return mkassert('bool', 'eq', lh, 'true')

def p_expr(p):
    res = []
    while True:
        if p.accept('('):
            res.append(p.m)
            while True:
                res.append(p_exprs(p))
                if p.accept('sep'):
                    res.append(p.m)
                else:
                    break
            res.append(p.expect(')'))
        elif p.lookahead('assert'):
            state = p.push()
            try:
                res.append(p_assert(p))
            except ParseFailure:
                p.pop(state)
                res.append(p.expect('assert'))
        elif p.accept('string', 'op', 'ws', None):
            res.append(p.m)
        else:
            return ''.join(res)

def p_exprs(p):
    res = []
    while True:
        res.append(p_expr(p))
        if p.accept('cmp', 'logic', ','):
            res.append(p.m)
        else:
            return ''.join(res)

def p_stmt(p):
    ws = p.accept('ws') or ''

    # memcmp(lh,rh,size) => 0?
    if p.lookahead('memcmp'):
        state = p.push()
        try:
            p.expect('memcmp') ; p.accept('ws')
            p.expect('(') ; p.accept('ws')
            lh = p_expr(p) ; p.accept('ws')
            p.expect(',') ; p.accept('ws')
            rh = p_expr(p) ; p.accept('ws')
            p.expect(',') ; p.accept('ws')
            size = p_expr(p) ; p.accept('ws')
            p.expect(')') ; p.accept('ws')
            p.expect('=>') ; p.accept('ws')
            p.expect('0') ; p.accept('ws')
            return ws + mkassert('mem', 'eq', lh, rh, size)
        except ParseFailure:
            p.pop(state)

    # strcmp(lh,rh) => 0?
    if p.lookahead('strcmp'):
        state = p.push()
        try:
            p.expect('strcmp') ; p.accept('ws') ; p.expect('(') ; p.accept('ws')
            lh = p_expr(p) ; p.accept('ws')
            p.expect(',') ; p.accept('ws')
            rh = p_expr(p) ; p.accept('ws')
            p.expect(')') ; p.accept('ws')
            p.expect('=>') ; p.accept('ws')
            p.expect('0') ; p.accept('ws')
            return ws + mkassert('str', 'eq', lh, rh)
        except ParseFailure:
            p.pop(state)

    # lh => rh?
    lh = p_exprs(p)
    if p.accept('=>'):
        rh = p_exprs(p)
        return ws + mkassert('int', 'eq', lh, rh)
    else:
        return ws + lh

def main(input=None, output=None, pattern=[], limit=LIMIT):
    with openio(input or '-', 'r') as in_f:
        # create parser
        lexemes = LEXEMES.copy()
        lexemes['assert'] += pattern
        p = Parser(in_f, lexemes)

        with openio(output or '-', 'w') as f:
            def writeln(s=''):
                f.write(s)
                f.write('\n')
            f.writeln = writeln

            # write extra verbose asserts
            write_header(f, limit=limit)
            if input is not None:
                f.writeln("#line %d \"%s\"" % (1, input))

            # parse and write out stmt at a time
            try:
                while True:
                    f.write(p_stmt(p))
                    if p.accept('sep'):
                        f.write(p.m)
                    else:
                        break
            except ParseFailure as e:
                print('warning: %s' % e)
                pass

            for i in range(p.off, len(p.tokens)):
                f.write(p.tokens[i][1])


if __name__ == "__main__":
    import argparse
    import sys
    parser = argparse.ArgumentParser(
        description="Preprocessor that makes asserts easier to debug.",
        allow_abbrev=False)
    parser.add_argument(
        'input',
        help="Input C file.")
    parser.add_argument(
        '-o', '--output',
        required=True,
        help="Output C file.")
    parser.add_argument(
        '-p', '--pattern',
        action='append',
        help="Regex patterns to search for starting an assert statement. This"
            " implicitly includes \"assert\" and \"=>\".")
    parser.add_argument(
        '-l', '--limit',
        type=lambda x: int(x, 0),
        default=LIMIT,
        help="Maximum number of characters to display in strcmp and memcmp. "
            "Defaults to %r." % LIMIT)
    sys.exit(main(**{k: v
        for k, v in vars(parser.parse_intermixed_args()).items()
        if v is not None}))
