b08463f8de
- Renamed explode_asserts.py -> pretty_asserts.py, this name is
hopefully a bit more descriptive
- Small cleanup of the parser rules
- Added recognization of memcmp/strcmp => 0 statements and generate
the relevant memory inspecting assert messages
I attempted to fix the incorrect column numbers for the generated
asserts, but unfortunately this didn't go anywhere and I don't think
it's actually possible.
There is no column control analogous to the #line directive. I thought
you might be able to intermix #line directives to put arguments at the
right column like so:
assert(a == b);
__PRETTY_ASSERT_INT_EQ(
#line 1
a,
#line 1
b);
But this doesn't work as preprocessor directives are not allowed in
macros arguments in standard C. Unfortunately this is probably not
possible to fix without better support in the language.
427 lines
14 KiB
Python
Executable File
427 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
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': [':', ';', '\{', '\}', ','],
|
|
}
|
|
|
|
|
|
def openio(path, mode='r'):
|
|
if path == '-':
|
|
if 'r' in mode:
|
|
return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
|
|
else:
|
|
return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
|
|
else:
|
|
return open(path, mode)
|
|
|
|
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 <stdio.h>")
|
|
f.writeln("#include <stdbool.h>")
|
|
f.writeln("#include <stdint.h>")
|
|
f.writeln("#include <inttypes.h>")
|
|
f.writeln("#include <signal.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', None, 'ws'):
|
|
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 f:
|
|
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 easy to debug.")
|
|
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', default=LIMIT, type=int,
|
|
help="Maximum number of characters to display in strcmp and memcmp.")
|
|
sys.exit(main(**{k: v
|
|
for k, v in vars(parser.parse_args()).items()
|
|
if v is not None}))
|