| #!/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})) |