diff options
Diffstat (limited to 'math/gen-libm-test.py')
-rwxr-xr-x | math/gen-libm-test.py | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/math/gen-libm-test.py b/math/gen-libm-test.py new file mode 100755 index 0000000000..8d156ea8d1 --- /dev/null +++ b/math/gen-libm-test.py @@ -0,0 +1,603 @@ +#!/usr/bin/python +# Generate tests for libm functions. +# Copyright (C) 2018 Free Software Foundation, Inc. +# This file is part of the GNU C Library. +# +# The GNU C Library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# The GNU C Library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with the GNU C Library; if not, see +# <http://www.gnu.org/licenses/>. + +import argparse +from collections import defaultdict +import re + + +# Sorted list of all float types in ulps files. +ALL_FLOATS = ('double', 'float', 'float128', 'idouble', + 'ifloat', 'ifloat128', 'ildouble', 'ldouble') + +# Map float types in ulps files to C-like prefix for macros. +ALL_FLOATS_PFX = {'double': 'DBL', + 'ldouble': 'LDBL', + 'float': 'FLT', + 'float128': 'FLT128'} + +# Number of arguments in structure (as opposed to arguments that are +# pointers to return values) for an argument descriptor. +DESCR_NUM_ARGS = {'f': 1, 'a': 1, 'j': 1, 'i': 1, 'u': 1, 'l': 1, 'L': 1, + 'p': 0, 'F': 0, 'I': 0, + 'c': 2} + +# Number of results in structure for a result descriptor. +DESCR_NUM_RES = {'f': 1, 'i': 1, 'l': 1, 'L': 1, 'M': 1, 'U': 1, 'b': 1, + '1': 1, + 'c': 2} + +# Rounding modes, in the form in which they appear in +# auto-libm-test-out-* and the order in which expected results appear +# in structures and TEST_* calls. +ROUNDING_MODES = ('downward', 'tonearest', 'towardzero', 'upward') + +# Map from special text in TEST_* calls for rounding-mode-specific +# results and flags, to those results for each mode. +ROUNDING_MAP = { + 'plus_oflow': ('max_value', 'plus_infty', 'max_value', 'plus_infty'), + 'minus_oflow': ('minus_infty', 'minus_infty', '-max_value', '-max_value'), + 'plus_uflow': ('plus_zero', 'plus_zero', 'plus_zero', 'min_subnorm_value'), + 'minus_uflow': ('-min_subnorm_value', 'minus_zero', 'minus_zero', + 'minus_zero'), + 'ERRNO_PLUS_OFLOW': ('0', 'ERRNO_ERANGE', '0', 'ERRNO_ERANGE'), + 'ERRNO_MINUS_OFLOW': ('ERRNO_ERANGE', 'ERRNO_ERANGE', '0', '0'), + 'ERRNO_PLUS_UFLOW': ('ERRNO_ERANGE', 'ERRNO_ERANGE', 'ERRNO_ERANGE', '0'), + 'ERRNO_MINUS_UFLOW': ('0', 'ERRNO_ERANGE', 'ERRNO_ERANGE', 'ERRNO_ERANGE'), + 'XFAIL_ROUNDING_IBM128_LIBGCC': ('XFAIL_IBM128_LIBGCC', '0', + 'XFAIL_IBM128_LIBGCC', + 'XFAIL_IBM128_LIBGCC') + } + +# Map from raw test arguments to a nicer form to use when displaying +# test results. +BEAUTIFY_MAP = {'minus_zero': '-0', + 'plus_zero': '+0', + '-0x0p+0f': '-0', + '-0x0p+0': '-0', + '-0x0p+0L': '-0', + '0x0p+0f': '+0', + '0x0p+0': '+0', + '0x0p+0L': '+0', + 'minus_infty': '-inf', + 'plus_infty': 'inf', + 'qnan_value': 'qNaN', + 'snan_value': 'sNaN', + 'snan_value_ld': 'sNaN'} + +# Flags in auto-libm-test-out that map directly to C flags. +FLAGS_SIMPLE = {'ignore-zero-inf-sign': 'IGNORE_ZERO_INF_SIGN', + 'no-test-inline': 'NO_TEST_INLINE', + 'xfail': 'XFAIL_TEST'} + +# Exceptions in auto-libm-test-out, and their corresponding C flags +# for being required, OK or required to be absent. +EXC_EXPECTED = {'divbyzero': 'DIVBYZERO_EXCEPTION', + 'inexact': 'INEXACT_EXCEPTION', + 'invalid': 'INVALID_EXCEPTION', + 'overflow': 'OVERFLOW_EXCEPTION', + 'underflow': 'UNDERFLOW_EXCEPTION'} +EXC_OK = {'divbyzero': 'DIVBYZERO_EXCEPTION_OK', + 'inexact': '0', + 'invalid': 'INVALID_EXCEPTION_OK', + 'overflow': 'OVERFLOW_EXCEPTION_OK', + 'underflow': 'UNDERFLOW_EXCEPTION_OK'} +EXC_NO = {'divbyzero': '0', + 'inexact': 'NO_INEXACT_EXCEPTION', + 'invalid': '0', + 'overflow': '0', + 'underflow': '0'} + + +class Ulps(object): + """Maximum expected errors of libm functions.""" + + def __init__(self): + """Initialize an Ulps object.""" + # normal[function][float_type] is the ulps value, and likewise + # for real and imag. + self.normal = defaultdict(lambda: defaultdict(lambda: 0)) + self.real = defaultdict(lambda: defaultdict(lambda: 0)) + self.imag = defaultdict(lambda: defaultdict(lambda: 0)) + # List of ulps kinds, in the order in which they appear in + # sorted ulps files. + self.ulps_kinds = (('Real part of ', self.real), + ('Imaginary part of ', self.imag), + ('', self.normal)) + self + + def read(self, ulps_file): + """Read ulps from a file into an Ulps object.""" + self.ulps_file = ulps_file + with open(ulps_file, 'r') as f: + ulps_dict = None + ulps_fn = None + for line in f: + # Ignore comments. + if line.startswith('#'): + continue + line = line.rstrip() + # Ignore empty lines. + if line == '': + continue + m = re.match(r'([^:]*): (.*)\Z', line) + if not m: + raise ValueError('bad ulps line: %s' % line) + line_first = m.group(1) + line_second = m.group(2) + if line_first == 'Function': + fn = None + ulps_dict = None + for k_prefix, k_dict in self.ulps_kinds: + if line_second.startswith(k_prefix): + ulps_dict = k_dict + fn = line_second[len(k_prefix):] + break + if not fn.startswith('"') or not fn.endswith('":'): + raise ValueError('bad ulps line: %s' % line) + ulps_fn = fn[1:-2] + else: + if line_first not in ALL_FLOATS: + raise ValueError('bad ulps line: %s' % line) + ulps_val = int(line_second) + if ulps_val > 0: + ulps_dict[ulps_fn][line_first] = max( + ulps_dict[ulps_fn][line_first], + ulps_val) + + def write(self, ulps_file): + """Write ulps back out as a sorted ulps file.""" + # Output is sorted first by function name, then by (real, + # imag, normal), then by float type. + out_data = {} + for order, (prefix, d) in enumerate(self.ulps_kinds): + for fn in d.keys(): + fn_data = ['%s: %d' % (f, d[fn][f]) + for f in sorted(d[fn].keys())] + fn_text = 'Function: %s"%s":\n%s' % (prefix, fn, + '\n'.join(fn_data)) + out_data[(fn, order)] = fn_text + out_list = [out_data[fn_order] for fn_order in sorted(out_data.keys())] + out_text = ('# Begin of automatic generation\n\n' + '# Maximal error of functions:\n' + '%s\n\n' + '# end of automatic generation\n' + % '\n\n'.join(out_list)) + with open(ulps_file, 'w') as f: + f.write(out_text) + + @staticmethod + def ulps_table(name, ulps_dict): + """Return text of a C table of ulps.""" + ulps_list = [] + for fn in sorted(ulps_dict.keys()): + fn_ulps = [str(ulps_dict[fn][f]) for f in ALL_FLOATS] + ulps_list.append(' { "%s", {%s} },' % (fn, ', '.join(fn_ulps))) + ulps_text = ('static const struct ulp_data %s[] =\n' + ' {\n' + '%s\n' + ' };' + % (name, '\n'.join(ulps_list))) + return ulps_text + + def write_header(self, ulps_header): + """Write header file with ulps data.""" + header_text_1 = ('/* This file is automatically generated\n' + ' from %s with gen-libm-test.py.\n' + ' Don\'t change it - change instead the master ' + 'files. */\n\n' + 'struct ulp_data\n' + '{\n' + ' const char *name;\n' + ' FLOAT max_ulp[%d];\n' + '};' + % (self.ulps_file, len(ALL_FLOATS))) + macro_list = [] + for i, f in enumerate(ALL_FLOATS): + if f.startswith('i'): + itxt = 'I_' + f = f[1:] + else: + itxt = '' + macro_list.append('#define ULP_%s%s %d' + % (itxt, ALL_FLOATS_PFX[f], i)) + header_text = ('%s\n\n' + '%s\n\n' + '/* Maximal error of functions. */\n' + '%s\n' + '%s\n' + '%s\n' + % (header_text_1, '\n'.join(macro_list), + self.ulps_table('func_ulps', self.normal), + self.ulps_table('func_real_ulps', self.real), + self.ulps_table('func_imag_ulps', self.imag))) + with open(ulps_header, 'w') as f: + f.write(header_text) + + +def read_auto_tests(test_file): + """Read tests from auto-libm-test-out-<function> (possibly None).""" + auto_tests = defaultdict(lambda: defaultdict(dict)) + if test_file is None: + return auto_tests + with open(test_file, 'r') as f: + for line in f: + if not line.startswith('= '): + continue + line = line[len('= '):].rstrip() + # Function, rounding mode, condition and inputs, outputs + # and flags. + m = re.match(r'([^ ]+) ([^ ]+) ([^: ][^ ]* [^:]*) : (.*)\Z', line) + if not m: + raise ValueError('bad automatic test line: %s' % line) + auto_tests[m.group(1)][m.group(2)][m.group(3)] = m.group(4) + return auto_tests + + +def beautify(arg): + """Return a nicer representation of a test argument.""" + if arg in BEAUTIFY_MAP: + return BEAUTIFY_MAP[arg] + if arg.startswith('-') and arg[1:] in BEAUTIFY_MAP: + return '-' + BEAUTIFY_MAP[arg[1:]] + if re.match(r'-?0x[0-9a-f.]*p[-+][0-9]+f\Z', arg): + return arg[:-1] + if re.search(r'[0-9]L\Z', arg): + return arg[:-1] + return arg + + +def complex_beautify(arg_real, arg_imag): + """Return a nicer representation of a complex test argument.""" + res_real = beautify(arg_real) + res_imag = beautify(arg_imag) + if res_imag.startswith('-'): + return '%s - %s i' % (res_real, res_imag[1:]) + else: + return '%s + %s i' % (res_real, res_imag) + + +def apply_lit_token(arg, macro): + """Apply the LIT or ARG_LIT macro to a single token.""" + # The macro must only be applied to a floating-point constant, not + # to an integer constant or lit_* value. + sign_re = r'[+-]?' + exp_re = r'([+-])?[0-9]+' + suffix_re = r'[lLfF]?' + dec_exp_re = r'[eE]' + exp_re + hex_exp_re = r'[pP]' + exp_re + dec_frac_re = r'(?:[0-9]*\.[0-9]+|[0-9]+\.)' + hex_frac_re = r'(?:[0-9a-fA-F]*\.[0-9a-fA-F]+|[0-9a-fA-F]+\.)' + dec_int_re = r'[0-9]+' + hex_int_re = r'[0-9a-fA-F]+' + dec_cst_re = r'(?:%s(?:%s)?|%s%s)' % (dec_frac_re, dec_exp_re, + dec_int_re, dec_exp_re) + hex_cst_re = r'0[xX](?:%s|%s)%s' % (hex_frac_re, hex_int_re, hex_exp_re) + fp_cst_re = r'(%s(?:%s|%s))%s\Z' % (sign_re, dec_cst_re, hex_cst_re, + suffix_re) + m = re.match(fp_cst_re, arg) + if m: + return '%s (%s)' % (macro, m.group(1)) + else: + return arg + + +def apply_lit(arg, macro): + """Apply the LIT or ARG_LIT macro to constants within an expression.""" + # Assume expressions follow the GNU Coding Standards, with tokens + # separated by spaces. + return ' '.join([apply_lit_token(t, macro) for t in arg.split()]) + + +def gen_test_args_res(descr_args, descr_res, args, res_rm): + """Generate a test given the arguments and per-rounding-mode results.""" + # Determine whether any arguments or results, for any rounding + # mode, are non-finite. (For consistency with the old perl + # script, this does not handle infinities resulting from + # ROUNDING_MAP.) + non_finite = False + test_snan = False + all_args_res = list(args) + for r in res_rm: + all_args_res.extend(r[:len(r)-1]) + for a in all_args_res: + if 'snan_value' in a: + test_snan = True + non_finite = True + elif 'qnan_value' in a or 'plus_infty' in a or 'minus_infty' in a: + non_finite = True + # Process the arguments. + args_disp = [] + args_c = [] + arg_pos = 0 + for d in descr_args: + if DESCR_NUM_ARGS[d] == 0: + continue + if d == 'c': + args_disp.append(complex_beautify(args[arg_pos], + args[arg_pos + 1])) + args_c.append(apply_lit(args[arg_pos], 'LIT')) + args_c.append(apply_lit(args[arg_pos + 1], 'LIT')) + else: + args_disp.append(beautify(args[arg_pos])) + if d == 'f': + args_c.append(apply_lit(args[arg_pos], 'LIT')) + elif d == 'a': + args_c.append(apply_lit(args[arg_pos], 'ARG_LIT')) + else: + args_c.append(args[arg_pos]) + arg_pos += DESCR_NUM_ARGS[d] + args_disp_text = ', '.join(args_disp).replace('"', '\\"') + # Process the results. + for rm in range(len(ROUNDING_MODES)): + res = res_rm[rm] + res_pos = 0 + rm_args = [] + ignore_result_any = False + ignore_result_all = True + special = [] + for d in descr_res: + if d == '1': + special.append(res[res_pos]) + elif DESCR_NUM_RES[d] == 1: + result = res[res_pos] + if result == 'IGNORE': + ignore_result_any = True + result = '0' + else: + ignore_result_all = False + if d == 'f': + result = apply_lit(result, 'LIT') + rm_args.append(result) + else: + # Complex result. + result1 = res[res_pos] + if result1 == 'IGNORE': + ignore_result_any = True + result1 = '0' + else: + ignore_result_all = False + result1 = apply_lit(result1, 'LIT') + rm_args.append(result1) + result2 = res[res_pos + 1] + if result2 == 'IGNORE': + ignore_result_any = True + result2 = '0' + else: + ignore_result_all = False + result2 = apply_lit(result2, 'LIT') + rm_args.append(result2) + res_pos += DESCR_NUM_RES[d] + if ignore_result_any and not ignore_result_all: + raise ValueError('some but not all function results ignored') + flags = [] + if ignore_result_any: + flags.append('IGNORE_RESULT') + if non_finite: + flags.append('NON_FINITE') + if test_snan: + flags.append('TEST_SNAN') + flags.append(res[res_pos]) + rm_args.append('|'.join(flags)) + for sp in special: + if sp == 'IGNORE': + rm_args.extend(['0', '0']) + else: + rm_args.extend(['1', apply_lit(sp, 'LIT')]) + for k in sorted(ROUNDING_MAP.keys()): + rm_args = [arg.replace(k, ROUNDING_MAP[k][rm]) for arg in rm_args] + args_c.append('{ %s }' % ', '.join(rm_args)) + return ' { "%s", %s },\n' % (args_disp_text, ', '.join(args_c)) + + +def convert_condition(cond): + """Convert a condition from auto-libm-test-out to C form.""" + conds = cond.split(':') + conds_c = [] + for c in conds: + if not c.startswith('arg_fmt('): + c = c.replace('-', '_') + conds_c.append('TEST_COND_' + c) + return '(%s)' % ' && '.join(conds_c) + + +def cond_value(cond, if_val, else_val): + """Return a C conditional expression between two values.""" + if cond == '1': + return if_val + elif cond == '0': + return else_val + else: + return '(%s ? %s : %s)' % (cond, if_val, else_val) + + +def gen_auto_tests(auto_tests, descr_args, descr_res, fn): + """Generate C code for the auto-libm-test-out-* tests for a function.""" + for rm_idx, rm_name in enumerate(ROUNDING_MODES): + this_tests = sorted(auto_tests[fn][rm_name].keys()) + if rm_idx == 0: + rm_tests = this_tests + if not rm_tests: + raise ValueError('no automatic tests for %s' % fn) + else: + if rm_tests != this_tests: + raise ValueError('inconsistent lists of tests of %s' % fn) + test_list = [] + for test in rm_tests: + fmt_args = test.split() + fmt = fmt_args[0] + args = fmt_args[1:] + test_list.append('#if %s\n' % convert_condition(fmt)) + res_rm = [] + for rm in ROUNDING_MODES: + test_out = auto_tests[fn][rm][test] + out_str, flags_str = test_out.split(':', 1) + this_res = out_str.split() + flags = flags_str.split() + flag_cond = {} + for flag in flags: + m = re.match(r'([^:]*):(.*)\Z', flag) + if m: + f_name = m.group(1) + cond = convert_condition(m.group(2)) + if f_name in flag_cond: + if flag_cond[f_name] != '1': + flag_cond[f_name] = ('%s || %s' + % (flag_cond[f_name], cond)) + else: + flag_cond[f_name] = cond + else: + flag_cond[flag] = '1' + flags_c = [] + for flag in sorted(FLAGS_SIMPLE.keys()): + if flag in flag_cond: + flags_c.append(cond_value(flag_cond[flag], + FLAGS_SIMPLE[flag], '0')) + for exc in sorted(EXC_EXPECTED.keys()): + exc_expected = EXC_EXPECTED[exc] + exc_ok = EXC_OK[exc] + no_exc = EXC_NO[exc] + exc_cond = flag_cond.get(exc, '0') + exc_ok_cond = flag_cond.get(exc + '-ok', '0') + flags_c.append(cond_value(exc_cond, + cond_value(exc_ok_cond, exc_ok, + exc_expected), + cond_value(exc_ok_cond, exc_ok, + no_exc))) + if 'errno-edom' in flag_cond and 'errno-erange' in flag_cond: + raise ValueError('multiple errno values expected') + if 'errno-edom' in flag_cond: + if flag_cond['errno-edom'] != '1': + raise ValueError('unexpected condition for errno-edom') + errno_expected = 'ERRNO_EDOM' + elif 'errno-erange' in flag_cond: + if flag_cond['errno-erange'] != '1': + raise ValueError('unexpected condition for errno-erange') + errno_expected = 'ERRNO_ERANGE' + else: + errno_expected = 'ERRNO_UNCHANGED' + if 'errno-edom-ok' in flag_cond: + if ('errno-erange-ok' in flag_cond + and (flag_cond['errno-erange-ok'] + != flag_cond['errno-edom-ok'])): + errno_unknown_cond = ('%s || %s' + % (flag_cond['errno-edom-ok'], + flag_cond['errno-erange-ok'])) + else: + errno_unknown_cond = flag_cond['errno-edom-ok'] + else: + errno_unknown_cond = flag_cond.get('errno-erange-ok', '0') + flags_c.append(cond_value(errno_unknown_cond, '0', errno_expected)) + flags_c = [flag for flag in flags_c if flag != '0'] + if not flags_c: + flags_c = ['NO_EXCEPTION'] + this_res.append(' | '.join(flags_c)) + res_rm.append(this_res) + test_list.append(gen_test_args_res(descr_args, descr_res, args, + res_rm)) + test_list.append('#endif\n') + return ''.join(test_list) + + +def gen_test_line(descr_args, descr_res, args_str): + """Generate C code for the tests for a single TEST_* line.""" + test_args = args_str.split(',') + test_args = test_args[1:] + test_args = [a.strip() for a in test_args] + num_args = sum([DESCR_NUM_ARGS[c] for c in descr_args]) + num_res = sum([DESCR_NUM_RES[c] for c in descr_res]) + args = test_args[:num_args] + res = test_args[num_args:] + if len(res) == num_res: + # One set of results for all rounding modes, no flags. + res.append('0') + res_rm = [res, res, res, res] + elif len(res) == num_res + 1: + # One set of results for all rounding modes, with flags. + if not ('EXCEPTION' in res[-1] + or 'ERRNO' in res[-1] + or 'IGNORE_ZERO_INF_SIGN' in res[-1] + or 'TEST_NAN_SIGN' in res[-1] + or 'NO_TEST_INLINE' in res[-1] + or 'XFAIL' in res[-1]): + raise ValueError('wrong number of arguments: %s' % args_str) + res_rm = [res, res, res, res] + elif len(res) == (num_res + 1) * 4: + # One set of results per rounding mode, with flags. + nr_plus = num_res + 1 + res_rm = [res[:nr_plus], res[nr_plus:2*nr_plus], + res[2*nr_plus:3*nr_plus], res[3*nr_plus:]] + return gen_test_args_res(descr_args, descr_res, args, res_rm) + + +def generate_testfile(inc_input, auto_tests, c_output): + """Generate test .c file from .inc input.""" + test_list = [] + with open(inc_input, 'r') as f: + for line in f: + line_strip = line.strip() + if line_strip.startswith('AUTO_TESTS_'): + m = re.match(r'AUTO_TESTS_([^_]*)_([^_ ]*) *\(([^)]*)\),\Z', + line_strip) + if not m: + raise ValueError('bad AUTO_TESTS line: %s' % line) + test_list.append(gen_auto_tests(auto_tests, m.group(1), + m.group(2), m.group(3))) + elif line_strip.startswith('TEST_'): + m = re.match(r'TEST_([^_]*)_([^_ ]*) *\((.*)\),\Z', line_strip) + if not m: + raise ValueError('bad TEST line: %s' % line) + test_list.append(gen_test_line(m.group(1), m.group(2), + m.group(3))) + else: + test_list.append(line) + with open(c_output, 'w') as f: + f.write(''.join(test_list)) + + +def main(): + """The main entry point.""" + parser = argparse.ArgumentParser(description='Generate libm tests.') + parser.add_argument('-a', dest='auto_input', metavar='FILE', + help='input file with automatically generated tests') + parser.add_argument('-c', dest='inc_input', metavar='FILE', + help='input file .inc file with tests') + parser.add_argument('-u', dest='ulps_file', metavar='FILE', + help='input file with ulps') + parser.add_argument('-n', dest='ulps_output', metavar='FILE', + help='generate sorted ulps file FILE') + parser.add_argument('-C', dest='c_output', metavar='FILE', + help='generate output C file FILE from .inc file') + parser.add_argument('-H', dest='ulps_header', metavar='FILE', + help='generate output ulps header FILE') + args = parser.parse_args() + ulps = Ulps() + if args.ulps_file is not None: + ulps.read(args.ulps_file) + auto_tests = read_auto_tests(args.auto_input) + if args.ulps_output is not None: + ulps.write(args.ulps_output) + if args.ulps_header is not None: + ulps.write_header(args.ulps_header) + if args.c_output is not None: + generate_testfile(args.inc_input, auto_tests, args.c_output) + + +if __name__ == '__main__': + main() |